За жизнь я программировал на разных языках программирования, включая Ассемблер, Си, Java и JavaScript. И в этой цепочке явно замечается, на сколько удобнее и быстрее идёт разработка на каждом следующем языке по сравнению с предыдущим. В этой статье я хочу поговорить о свойствах последнего из них, а точнее об одном свойстве – неявной (утиной) типизации, и развить мысль дальше. Это послание к создателям языков и размышление на тему того, каким должен быть современный язык программирования. Если у вас есть свободные 10 минут – прошу под кат.
Многие ещё помнят, когда-то в базах данных нужно было задавать размеры текстовых полей, программистам приходилось думать, а хватит 20 символов для имени пользователя или не жадничать и дать 255. Сейчас у нас есть NoSQL базы, в которых можно не думать не только о размере полей, но и о структуре таблиц, добавляя поля в каждую запись индивидуально. Конечно иногда приходится проводить оптимизации, использовать старые, но эффективные инструменты, прописывать длину полей, индексы и т.п… Но здесь есть принципиальный момент – эта работа делается только по необходимости. Это экономит самый важный ресурс – наше время.
К чему это отступление? Сейчас у архитекторов языков наметился интересный тренд, на типизацию JavaScript, давно существует CoffeeScript со встроенными классами, EcmaScript 6 объявил для них нативную реализацию, TypeScript ввёл типизацию в название. Не удивительно, ведь не типизованный язык сложнее компилировать, сложнее проводить его статический анализ и искать ошибки. И безусловно классы – это то, чего в JavaScript давно не хватало. Сейчас меня закидают тапками, но какова же обратная сторона? Эта ситуация напоминает переход от NoSQL баз данных назад к структуре. Это понятнее, производительнее, но медленнее в разработке. И хорошо, если язык разрешает расширение объявленных экземпляров класса и использование нетипизованных хешей, в TypeScript, например, с этим наблюдались проблемы. Ждём, что покажет ES6.
Почему я об этом так беспокоюсь? Я решал проблему нетипизованности JavaScript в большом финансовом проекте, где нужна была абсолютная стабильность. Многие знают как, используя JSDocs. Вы просто пишите типы в специальных комментариях, объявляете классы, наследование, добавляете описание функций, классов и модулей. Немного неудобно, кажется, что делаешь двойную работу, зато можно писать хоть ERP-систему. На этот раз я решил покрыть весь код, чтобы не осталось ни одного неоттипированного кусочка. Проблему начались когда я начал спускаться на уровни глубже модулей и классов, к функциям, их аргументам, их преобразованиям. Сущности начали плодиться как грибы, на 15 строчек JavaScript кода приходилось по 3 класса и 2 сущности обработчика, и по 30 строчек JSDocs комментариев. Некоторые участки были более плодовитыми, некоторые менее, но тренд явно прослеживался. Одно время назад знакомый джавист жаловался, что на то, что он может собрать на HTML за 2 недели на Java ему приходится тратить 2 месяца. И сейчас я понял почему. Это не проблема Java – это проблема всего типизованного подхода. Многие объекты создаются всего один раз, зачем создавать для них класс? Существует огромное количество мелких объектов из двух-трёх свойств, если создавать класс для каждого из них – мы закопаемся в этом зоопарке. В JavaScript распространён паттерн параметра-конфигурации, когда мы посылаем в функцию хеш из свойств, а функция уже сама реагирует на какие их этих свойств реагировать. Кроме того существуют сквозные обработчики таких свойств, которые принимают хеш и перенаправляют кому-то, даже не зная что внутри. Отсутствие типизации – это замечательное свойство языка, которое кардинально экономит время программисту.
Чего бы хотелось? Хотелось бы типизации по запросу. Знаете в чём основная причина возникновения ООП? Я сам был удивлён, когда узнал. Можно отлично писать программы и в процедурном стиле, они получатся не менее эффективными, компиляторы прекрасно их съедят, а функционал они смогут выполнять полностью тот же самый. Основная причина – когда мы группируем свойства и методы в одну сущность с говорящим названием – нам проще это понимать. Вот это Лисица, она наследуется от Животного, она имеет метод «съесть», который принимает экземпляр класса Заяц. Это проще запоминается, вот и всё. Всё это сделано для нас, чтобы экономить наши мыслительные усилия. Есть и другие причины, чтобы избежать конфликтов имён и, чтобы обращаться к свойствам внутри класса по короткому имени, но они могут быть решены гораздо более простыми способами. Я работал в проекте, писавшем в процедурном стиле, и там модуль с 2000 функций, конфликта имён не возникало, а обращаться к ним без префикса названий классов было даже быстрее. Единственная настоящая причина существования классов – они делают программу проще для понимания. Поэтому, следуя этой логике, должны применяться на объектах, которые действительно представляют из себя законченные сущности, которые мы хотим выделить и дать им название. Но нужно оставить возможность использовать и нетипизованные хеши, принимающие свойствами примитивы, функции, как объекты первого рода, или другие хеши. Это огромный шаг вперёд и не нужно от него отказываться.
Даже больше того, нам нужно больше таких фич. Пускай observable будет встроен во все объекты нативно и будет вызываться по требованию (кажется над этим уже работают). Пускай компилятор сам определяет границы функций и служебных блоков (а что, думаете невозможно?). Пускай callback-hell и клёвые, но громоздкие promises, решаются средствами языка в одну строчку. Ветвления логики сейчас разруливаются или через if, или через наследование или агрегацию, если заранее подумать. А как бы сделать так, чтобы заранее не думать и совместить плюсы обоих подходов? Хотим больше синтаксического сахара, хотим [a, b] = [2, 3] (уже делается). Хотим кроме иерархического ещё и лексический this (видимо под другим именем). Хотим передавать текущий скоуп (и доступ ко всем замыканиям) как аргументы в другую функцию. Понимаю, что сложно. Много работы по оптимизации, сложный инженерный вызов. Но отцы же наши подарили нам утиную типизацию, должно и наше поколение внести свой вклад.
Коллеги, вопрос к каждому, а чего лично вам не хватает в современном используемом вами языке программирования? Чего хотелось бы добавить? Что облегчило бы вам жизнь? Какие рутинные операции часто приходится делать? Из каких повторяющихся элементов состоит ваш код, от которых сейчас нельзя избавиться? В каких местах часты ошибки, а хотелось бы, чтобы компилятор за этим следил? Что смогло бы сэкономить ваше время и избавить от остатков манки-кодинга? Что смогло бы повысить вашу продуктивность в разы (или на проценты) и сделать из вас супер-rockstar-ниндзя (или просто повысить ценность и цену на рынке труда)? Чего вам всегда хотелось, но казалось, что это никогда не добавят, потому что это слишком сложно? Или настолько просто, что никто этого не видит? Или просто каким вы видите хороший язык программирования? Марафон мнений объявляю открытым.
P.S.: Прошу строго не судить, если не согласны с какими-то тезисами статьи. Для того и публикую на geektimes, что статья скорее не техническая, но мечтательская. А мечтать каждый из нас волен в любом направлении. Однажды из таких мечтаний и вырастает будущее.
Комментарии (28)
nehaev
14.04.2015 19:31+2> Ассемблер, Си, Java и JavaScript. И в этой цепочке явно замечается, на сколько удобнее и быстрее идёт разработка на каждом следующем языке по сравнению с предыдущим.
Что-то у вас прогресс остановился 20 лет назад. И после JavaScript появлялись хорошие языки. Из тех которые нравятся лично мне: C#, Scala.rboots Автор
14.04.2015 22:16+1С# абсолютно шикарен в связке с Visual Studio, даже generic-типы ввели, но функции объектами первого рода не сделали, это вызывает большие неудобства, JavaScript в этом плане остаётся наиболее гибким. За Scala не могу сказать, не пробовал
kosmos89
15.04.2015 02:18Что значит не объекты первого рода?
Это не считается: ideone.com/ebjCzO?rboots Автор
15.04.2015 13:53Нет, лямбды не считаются, написать сколько-нибудь сложную логику в них проблематично. Плюс бывают случат когда нужно передать уже существующую или вообще стандартную функцию, например console.log, как колбек, во время отладки. Не видел в C# способа сделать это без дополнительных обёрток.
NeoCode
14.04.2015 19:55Очень интересно, и мне очень близка эта тема. Спасибо!
Но очень хочется более развернутых примеров. Но давайте по отдельности:
Ветвления логики сейчас разруливаются или через if, или через наследование или агрегацию, если заранее подумать. А как бы сделать так, чтобы заранее не думать и совместить плюсы обоих подходов?
хотя-бы как это может выглядеть синтаксически?
Хотим кроме иерархического ещё и лексический this
поясните что вы имеете в виду.
Хотим передавать текущий скоуп (и доступ ко всем замыканиям) как аргументы в другую функцию
это более-менее понятно… вы хотите сделать «текущий скоуп» первоклассным объектом? Ведь явная передача в другую функцию подразумевает что эту сущность можно сохранить в переменной.rboots Автор
14.04.2015 22:02+1— хотя-бы как это может выглядеть синтаксически?
что-то типа времменно прикрепляемых миксинов, или миксинов на лету. Например: есть объект Вася, принадлежит классу Человек, Вася пошёл на работу и на эти 8 часов он расширен классом Программист, от может кодить, компилировать или общаться с коллегами. Потом Вася ушёл с работы, сел в метро и на некоторое время расширился классом Пассажир, приобрёл все его свойства.
— поясните что вы имеете в виду.
Да наверное тот же скоуп, плюс возможно информация с явном виде внутри какой функции эта переменная была объявлена, куда вложена функция и т.п… А то даже для того, чтобы получить строчку в исходнике, где исполняется код, приходится ошибку кидать и трейс парсить.
— это более-менее понятно… вы хотите сделать «текущий скоуп» первоклассным объектом? Ведь явная передача в другую функцию подразумевает что эту сущность можно сохранить в переменной.
точно.NeoCode
14.04.2015 22:59Все-таки я не понял с иерархическим и лексическим this. Поясните на примере псевдокода чтоли. Я как-то не понял даже что это такое — «иерархический» и «лексический» this.
С временно прикрепляемым миксинами хорошая идея, на самом деле перекликается с возможностью Objective C добавлять методы на лету, но там вроде только в классы, а вы предлагаете в объекты… Это уже прототипное ООП. Интересно подумать над этим.
Со скоупами как объектами… сложновато, хотя физически, когда происходит замыкание лямбда-функции на локальные переменные, что-то подобное делается неявно. Я даже не могу сказать, хорошо это или плохо, нужно много примеров поясняющих данную фичу.rboots Автор
14.04.2015 23:49+1Иерархический:
var a = {
prop: 22,
func: function () {
console.log(this.prop); // 22
}
}
то есть this в обычном понимании.
Лексический:
var b = 33;
function eee () {
var c = 44;
console.log(lex_this.c); // 44
console.log(lex_this.parent.b); // 33
console.log(lex_this.environment); // {type: 'function', name: 'eee',file:'myfile.js',line:27,parent:[Object object]}
}NeoCode
15.04.2015 10:17Понятно, я на С++ пишу, поэтому мне такое слегка непривычно.
В js получается, что «иерархический» this это скорее доступ к объемлющей сущности, в языке D есть близкое понятие «outer» (хотя и не совсем то — там они не распространили эту логику на функции).
А «лексический» это вообще интересно, вроде как доступ к текущему стековому фрейму + текущая метаинформация, в С++ вообще нет аналогов… но выглядит очень даже ничего, спасибо за интересную идею!
Amomum
15.04.2015 13:46Наверное, я ретроград, но я ненавижу динамическую типизацию. Писать, когда она есть, действительно быстро, я не спорю. Если писать очень маленький код и не использовать стороннего. Какой-нибудь скрипт накидать по-быстрому.
А дальше (лично у меня) начинаются проблемы. Очень легко сделать опечатку в имени переменной и не заметить. Если чужой метод не откомментирован, то просто понять, что же у него за аргументы и что он возвращает, становится очень тяжело. И автоподстановка в IDE толком не работает, ведь IDE не может угадать тип (даже если мне он известен).
И чтобы напороться на ошибку, код приходится запускать.
Наверное, я что-то делаю неправильно? (Писал на python в pyCharm, javascript'a не знаю.)rboots Автор
15.04.2015 14:03Не скажу за питон, а в JavaScript в WebStorm IDE автоподстановка всё таки отлично помогает избежать опечаток, хоть и выводит все свойства, актуальные и нет. Из больших проектов писал биржевой терминал на 300 классов и коммерческую PvP-игру примерно классов на 500 — проблем не возникало. Если приложение ещё больше — можно разбить на отдельные сервисы и интегрировать их друг с другом, и, впринципе, проблем с управлением сложностью не должно возникнуть.
Amomum
15.04.2015 14:14+1А как автоподстановка узнает тип?
Вот есть у меня функцияdef foo(a, b) a. # <---- и какой тип у а? Откуда IDE знает, если вызова функции еще нет (или он в другом модуле)?
rboots Автор
20.04.2015 19:20Приближённые эвристические алгоритмы, раскручивает программу в обратном порядке, а так как на 100% это сделать часто нельзя — делает предположения. Плюс выводит как варианты все используемые имена функций (мне кажется там тоже есть какой-то умный фильтр, чтобы совсем неподходящие варианты не выводить). Вобщем, ребята из JetBrains знают своё дело.
Amomum
20.04.2015 20:52+1Хочется только поразиться, насколько это проще делать со статической типизацией :) Что человеку, что машине.
NeoCode
15.04.2015 14:36+1ИМХО, самое лучшее — статическая типизация + специальный динамический тип (any, variant). При желании можно хоть всю программу написать в динамическом стиле:) Хотя на практике необходимость в истинно динамических типах возникает не так уж и часто.
Кроме того, существует аналог утиной типизации для статической типизации — «структурная типизация», применяемая в Go и даже в C++ при работе с шаблонами.
k12th
Да, это очень удобно писать.
А потом ты приходишь на проект без единой строчки комментов и начинаешь распутывать, какие параметры из каких хешей кому нужны — очень весело.
scramble
просто организуйте хеш, разделяя на секции, и не пишите длинных одноуровневых мега-конфигов.
k12th
Это вы не мне советуйте, а тем, кто уже наколбасил.
scramble
я чувствую вашу боль, но это вопрос все же к культуре программирования, технология не виновата.
k12th
Согласен. И все-таки если б нужно было явно каждый ключ в структуре описывать, было б хотя бы понятнее, где что требуется.
rboots Автор
Та же проблема, когда запускаешь программу из командной строки, а у неё отсутствует помощь и список параметров. В общем случае это не решить средствами языка, это был бы костыль, тут скорее вопрос к культуре программирования.
Mithgol
Увы, документировать надо.
Потому что «проект без единой строчки комментов» наверняка испытывать будет и проблемы более сложные, чем непонятность параметров.
NeoCode
Просто пишите комменты:)
Кстати, по комментариям в коде тоже можно много чего интересного придумать. Все эти парадигмы, ООП, ФП, МП — касаются только кода, а комментарии как были простыми текстовыми строками так и остались. Конечно, это в большей степени относится к IDE, чем к языку…
k12th
Иногда и к языку.
В Python комментарий, идущий первой строкой в теле функции, доступен как свойство __doc__ (если не ошибаюсь). Схожий механизм есть и в Clojure.
NeoCode
Любопытно, надо взять на заметку. Но я скорее имею в виду наличие мощных инструментов для работы с документацией к проекту в целом и с комментариями в частности, интегрированные в среду разработки и компилятор. Комментарии из текста должны превратиться во что-то вроде базы знаний, интерактивно доступной с помощью специальных средств IDE. С иерархическим деревом-аутлайнером, с тегами, с автоматической проверкой соответствия кода (функции, класса) и комментария к нему (по прототипам, по дате модификации...), со встроенными средствами поддержки разного рода диаграмм, с возможностью «пользовательской разметки» (пользовательские комментарии к глобальным комментариям — весьма полезно когда новичек изучает существующий код) и т.д.
Все что сейчас есть это статическая генерация html по документирующим комментариям (doxygen и т.п.), и выглядит все это как инородная хрень.
k12th
Ну у меня WebStorm довольно хорошо обрабатывает JsDoc, улучшает интеллисенс, позволяет переходить по ссылкам, если они есть.