Кто знает о JS больше, чем один из его «отцов»? На HolyJS 2017 Piter приедет легендарный Дуглас Крокфорд, создатель JSON и автор множества инструментов JavaScript. В преддверии его выступлений в Петербурге публикуем перевод его выступления на .concat() 2015: The Better Parts — о том как использовать существующие языки программирования более эффективно и каким будет язык программирования будущего. Хотя с момента выступления прошло более года, доклад коснулся ряда «вечных» вопросов программирования, которые, мы уверены, будут актуальны и через 3-5 лет.
Этот человек — известный авиатор и писатель Антуан де Сент-Экзюпери. Большинство его помнит за то, что он является автором замечательной детской книги «Маленький принц» (хотя книга эта не совсем детская). Помимо нее он написал много других книг, и в одной из них есть удивительная фраза: «Как видно, совершенство достигается не тогда, когда уже нечего прибавить, но когда уже ничего нельзя отнять».
Это блестящая цитата. Она использовалась в разговорах о дизайне, архитектуре, притягивалась ко всему, что сочетает в себе креативность и дисциплину.
Он говорил о конструкции самолетов. Но, кажется, мысль на самом деле шире. Я думаю, что она лучшим образом подходит к компьютерной программе. Поскольку у нас особые отношения с совершенством, отсутствующие в других дисциплинах: то, что мы пишем, должно быть совершенным, или оно будет вести себя неправильно. И автор дает нам некоторое представление о том, как мы достигаем совершенства — через вычитание.
Я думаю, что это относится и к языкам программирования.
Языки программирования имеют тенденцию к постепенному усложнению. Но если мы хотим достичь совершенства, необходимо удалить оттуда некоторые вещи. В этом и заключается принцип сильных сторон. Согласно этому принципу, если функция в одних случаях полезна, а в других — опасна, при этом есть лучший вариант, всегда используйте лучший вариант.
Это удивительно противоречивое заявление. Есть множество людей, не желающих использовать лучший вариант. И происходит это из-за непонимания одной вещи: нам не платят за использование каждой функции языка. В конце проекта нет менеджера с портфелем, проверяющего: «А использовали ли вы двойное равенство»? Никто не заботится об этом. Нам платят за то, чтобы мы писали программы, которые хорошо работают и не содержат ошибок.
Когда «отсутствие ошибок» стало частью сделки? Оно всегда была ее частью. Просто мы очень редко достигаем этого. Легко забыть, что на самом деле отсутствие ошибок было первым требованием. Поэтому хороший язык программирования должен научить вас этому.
Я призываю людей изучать как можно больше языков программирования, потому что каждый из них даст вам новые идеи, которые вы сможете применить к другим языкам.
Язык, который научил меня больше всего, — это JavaScript. У меня было много времени, чтобы разобраться с ним, потому что я допустил каждую ошибку, которую только можно допустить в JavaScript. И начал с самой первой — наихудшей — я не удосужился выучить язык до того, как начал писать в нем.
В конце концов я изучил язык, а он научил меня. И продолжает меня учить. Я использовал этот язык для написания инструмента под названием JSLint, который читает программы JavaScript и подсказывает, как сделать их лучше. И JSLint научил меня еще больше. Все это изменило мою точку зрения на программирование, так что теперь моя основная цель — пытаться создавать программы, которые не содержат ошибок. И JSLint дал мне много информации о том, как это делать.
Я написал книгу о своем опыте использования JavaScript и кода JSLint — «JavaScript — сильные стороны». Возможно, вы слышали о ней. Она по-прежнему бестселлер, что редкость для книг по программному обеспечению. Большинство книг по программному обеспечению устаревают еще до выпуска, а эта по-прежнему актуальна. Все потому, что «сильные стороны» по-прежнему остаются сильными сторонами. Что хорошо, язык не изменился вообще.
Теперь аргументы против использования «сильных сторон». Я бы хотел изложить их для вас:
Итак, целью языков программирования является помощь разработчикам в создании программ, не содержащих ошибок.
Раньше мы думали, что писать хорошие программы на JavaScript просто невозможно, потому что это был такой хрупкий язык. Но оказывается, что писать хорошие программы на JavaScript не только невозможно, но и не нужно. Как раз из-за своей «хрупкости» JavaScript требует большей дисциплины, чем любой другой язык программирования. И вы действительно должны ее придерживаться, чтобы писать нечто, что будет работать.
Есть две вещи, которые работают против этого. Во-первых, это фантазия о непогрешимости. В особенности молодые программисты считают, что их навыки настолько развиты, что они могут делать удивительные вещи, которые будут работать. Во-вторых, есть бесполезность безупречности, которую вы видите особенно у старых парней, которые делали годами то, что никогда не работало. Эти два очень разных взгляда приводят к одному и тому же: danger driven development. И это плохо.
Одна из вещей, усложняющих управление разработкой программного обеспечения, — это сложность планирования. Есть два времени, о которых вы должны знать. Время A — это время, необходимое для написания кода. Мы действительно плохо оцениваем его — у нас нет науки, которая ответила бы на вопрос, как оценивать время A. Но еще хуже обстоит дело с оценкой времени B — времени, необходимого на то, чтобы заставить код работать правильно.
Время B должно быть равно 0, верно? Вы пишете код — он должен работать. Но время В иногда становится больше времени А. Иногда оно бесконечно. Подобное происходит, когда у вас есть проект по разработке ПО, который заканчивается, но затем от ПО отказываются, прежде чем оно начинает работать.
Все, что вы делаете в рамках времени A, увеличивающее время B, неправильно. Вы должны пытаться сократить время B до нуля.
Есть ряд дополнений к JavaScript, которые будут отправлены на ECMA General Assembly в июне этого года (эти нововведения действительно прошли через процедуру сертификации и стали частью новой спецификации — ECMAScript 2015 Language Specification, — прим. ред.).
Рад сообщить, что там есть некоторые новые сильные стороны. И сегодня я бы хотел поделиться ими с вами.
Первое и лучшее — это конструкция, называемая хвостовой рекурсией. Если последнее, что делает функция, возвращает результат вызова функции (которая может быть вызовом другой функции), вместо создания обратной последовательности вызова компилятор генерирует скачок. Так что код будет работать немного быстрее, что приятно. Но что еще лучше — для некоторых моделей это значительно уменьшит потребление памяти. Таким образом, можно использовать совершенно новый класс алгоритмов.
Например, мы можем использовать стиль передачи продолжений и другие виды программирования, которые мы не могли реализовать на старом языке. Так что с этой функцией JavaScript, наконец, становится реальным функциональным языком программирования, и это здорово.
Также мы получили оператор многоточия. Если мы поместим его в список параметров или аргументов, сможем иметь дело с переменным количеством аргументов, что действительно приятно.
Здесь у нас есть две версии функции curry. Первая — в соответствии с ES6:
Вторая — как это реализовывалось ранее:
Я не буду объяснять, что именно происходит во второй версии, потому что все это просто ужасно. Первая, наоборот, выглядит довольно разумно: всюду мы видим точки (здесь у вас может быть столько аргументов, сколько вы захотите). И это замечательно.
Нововведение не позволяет нам делать то, что мы не могли делать ранее. Но когда приходится иметь дело с переменным числом аргументов, такой подход намного приятнее.
Теперь у нас появились модули. Вы можете импортировать и экспортировать значения из разных файлов; и это наконец-то имеет поддержку в языке. Раньше все это реализовывалось через глобальные переменные, и это было ужасно.
Теперь мы можем сделать это правильно. Если вам не кажется, что это здорово, попробуйте реализовать то же самое в соответствии с предыдущим стандартом. Но с ES6 мы наконец можем делать это правильно на полностью асинхронном языке, без блокировок.
У нас появилось два новых способа определения переменных: let и const, что решает проблему области видимости блока. Оказывается, в хорошей программе вам не нужна область видимости блока. Но синтаксис JavaScript ранее выглядел так, будто область видимости блока есть (с его синтаксической конструкцией var), хотя это было не так, что смущало людей. Всякий раз из-за путанницы возникали ошибки. Let и Const позволяют этого избежать.
Опять же, новые конструкции не позволяют писать какие-либо программы, которые мы не могли написать раньше. Но теперь код не смущает Java-программистов, и это хорошо.
У нас появилась деструктуризация, которая представляет собой еще одну синтаксическую конфетку. Она также не позволяет вам делать то, что вы не могли сделать раньше. Но для некоторых вещей новый синтаксис намного более выразителен.
Например, здесь у меня есть объект. И я хочу создать некоторые переменные, инициализировав эти переменные из свойств объекта.
Это очень упрощенный пример. Позже я покажу вам еще один пример того, как вы могли бы использовать это.
У нас появился WeakMap. WeakMap работает так, как должны были бы работать объекты. В объектах JavaScript ключи — это строки, что является ошибкой. Было бы лучше, если бы вместо них использовались какие-то значения. Но, тем не менее, это строки. WeakMap решает эту проблему. Здесь можно взять любое значение и использовать его в качестве ключа.
К сожалению, нам пришлось добавить эту вещь, и она значительно усложнила язык. Кроме того, мы назвали его худшим именем из когда-либо использованных в языках программирования (weak — слабый, англ.). Никто не захочет помещать в свою программу что-то слабое. Но работает эта конструкция действительно хорошо.
С помощью WeakMap вы можете писать программы, которые невозможно было создать на языке ранее.
И наконец мы нечто, что я назвал мегастроковым литералом (в языке это называется шаблонными строками, но мне не нравится это имя; но ранее их называли квази-литералами, что еще больше сбивало с толку).
Это регулярные выражения, которое соответствует нескольким строкам в ES6. Объявление этого регулярного выражения просто ужасно; я надеюсь, когда-нибудь мы сделаем что-нибудь получше. Но в ES6 у нас нет лучшей альтернативы.
Итак, у нас есть функция, которая принимает строку и преобразовывает ее в регулярное выражение, предварительно удалив все пробелы. Имея такую функцию, я могу взять любое выражение, но не писать все в кучу, а поместить в него необходимое пустое пространство, чтобы видеть элементы и то, как они соотносятся друг с другом. Результат будет тем же самым.
У этой конструкции есть один недостаток — компилятор рассматривает регулярное выражение как строку, поэтому не может выполнить проверку. Валидация не произойдет, пока мы не вызовем конструктор. Но в предыдущей версии записи из-за того, что все было смешано в кучу, серьезные ошибки также могли легко проскочить мимо компилятора. Поэтому я не думаю, что мы потеряем много в этом преобразовании.
Кстати, если вы часто работаете с регулярными выражениями, я настоятельно рекомендую инструмент под названием RegulEx. Поместите в него регулярное выражение, и он построит его диаграмму, чтобы вы точно видели, что именно происходит внутри. Я использую его каждый раз, когда я пишу регулярные выражения.
Еще одна новая функция, которую мы получили а ES6, это анонимные стрелочные функции. Хорошей мотивацией для их появления стало то, что некоторые люди жаловались на большой объем текста при наборе названий функций. Поэтому я добавил такую вещь. По сути это более короткое обозначение для функции. В результате я получаю функцию, которая будет принимать аргумент и возвращать объект, у которого указанное свойство имеет это значение.
Все хорошо, кроме того, что этого не работает должным образом. Если вы вызовете эту функцию, она вернет undefined вместо значения, потому что здесь присутствует ошибка. Так что это еще одна из тех маргинальных вещей, которые иногда хороши, а иногда — нет. Было бы здорово, если бы это работало все время, но это не так.
Помимо сильных сторон, у ES6 есть и откровенно плохие элементы.
Худшее нововведение — это классы. Класс был самой востребованной новой функцией, причем большая часть запросов поступала от Java-программистов, которые теперь должны писать на JavaScript и они действительно возмущены этим. Хочется писать на Java, но задачи и деньги есть в JavaScript, поэтому им приходится это делать. Переучиваться они не хотят, поэтому новый синтаксис упростит их жизнь.
Все бы ничего, но полагаясь на этот новый синтаксис, они никогда не поймут, как работает язык. И они никогда не поймут, как эффективно использовать этот язык, хотя будут думать, что понимают. Они так и продолжат писать, не зная, насколько они несчастны. Это ловушка.
Я пересматриваю то, что было написано в «Сильных сторонах», учитывая новые знания и необходимость отражения нововведений ES6. Когда я писал «сильные стороны», я рекомендовал вместо классов использовать object.create.
Фактически, именно мне удалось добавить object.create к языку, чтобы именно я мог его использовать. Получилось неплохо. Но как же я был удивлен, когда заметил, что прекратил использовать object.create. Я добавил его туда для себя, но даже я его не использую. И причина в том, что я прекратил использовать this.
Я перестал использовать this из-за того, что в 2007 году сделал проект под названием ADSafe. В то время существовало несколько исследовательских групп, таких как fbjs в Facebook, Caja Google, Web Sandbox в Microsoft, кроме того, был мой собственный проект — ADSafe и другие. Все мы пытались выяснить, как сделать JavaScript безопасным языком, чтобы можно было добавить сторонний код в приложение и быть уверенным, что он не породит проблем с безопасностью. И в JavaScript это оказалось очень сложной задачей.
Одна из вещей, которая затрудняет ее решение — this. Если у вас есть this в методе, оно привязывается к списку событий объекта (это хорошо и необходимо). Но если вы вызываете тот же метод в качестве функции, this привязывается к глобальному объекту, что полностью нарушает нашу безопасность. Как с этим справиться?
Большинство проектов справились с этой проблемой при помощи компилятора, который переводит JavaScript в JavaScript со множеством рантайм-проверок и взаимодействий, чтобы предотвратить проблемы с безопасностью.
Мой подход в ADSafe был намного проще. Я просто сделал это незаконным, побудив отказаться от программ, где это используется. Это работает. Единственная проблема в том, что отказаться приходится от львиной доли программ, потому что все использовали this.
Однако моя гипотеза заключалась в том, что если удалить this из языка, мы все равно останемся с функциональным языком программирования, которого достаточно для написания хороших программ. Чтобы проверить ее, я начал писать также (без this) в свободном от ограничений JavaScript. И я был очень удивлен, обнаружив, что стал писать лучшие программы с меньшими усилиями, не имея this. Очень мило. Что и говорить, если наличие this в языке само по себе вызывает сложности: общаясь на английском, я не могу заранее знать, имеется в виду конструкция языка или местоимение. Это довольно сложно.
Я перестал использовать null. JavaScript имеет два минимальных значения — null и undefined. В некоторых языках считается, что у вас и одного не должно быть. JavaScript — это единственный язык, который у которого таких значений более одного. Но совершенно точно, что глупо иметь два сразу.
Некоторые фреймворки пытаются рассматривать их как взаимозаменяемые, но они таковыми не являются. В зависимости от ситуации я должен использовать только один из них. Я использовал undefined, потому что именно его использует сам язык: вы получаете undefined, если, к примеру, обращаетесь к отсутствующему свойству.
Неопределенное значение порождает целый круг проблем: совершенно неправильный тип отсутствующего объекта. Мы надеялись, что это будет исправлено в ES6, но этого не произошло. Наверное, так будет всегда. Но если вы не используете null, то не столкнетесь с этой проблемой. И я перестал использовать ошибочные значения, решив, что само их существование в JavaScript — плохая идея.
Изначально эта идея появилась из C, который использует 0 для представления no, false и другие аналогичные значения. JavaScript попытался сделать то же самое. Но это сбивает с толку.
Я перестал использовать for. В ES5 мы ввели новый набор методов работы с массивами — for each, map и другие, поэтому теперь я использую эти инструменты. Если у меня есть массив и мне нужно его обработать, я использую один из этих методов. И я могу объединить их вместе удобным образом. Это действительно выразительно.
Я не использую for each. В ES5 мы получили object .keys(object). Эта конструкция дает вам хороший массив строк. И он не включает в себя вещи из цепочки прототипов, поэтому вам не нужно фильтровать результаты. Очень приятно, что я могу взять массив и выполнить for each таким образом, это действительно выразительный способ.
Ранее я уже упоминал, что в ES6 появились правильная хвостовая рекурсия. И сейчас пора перестать использовать циклы вовсе (в частности, оператор while) — время использовать только рекурсивные функции.
Например, это рекурсивная функция. Вы вызываете эту функцию до тех пор, пока она не вернет undefined. Первая версия кода написана с использованием цикла while.
Вторая версия написана с использованием хвостовой рекурсии.
В ES6 эти два способа должны работать с одинаковой скоростью, потребляя ровно столько же памяти. Таким образом, у циклов перед рекурсией больше нет преимущества. Это здорово.
Я много думал о языке программирования следующего поколения. Каким он будет? Вероятно, это должен быть язык, которым мы заменим JavaScript, потому что было бы очень грустно, если бы оказалось, что JavaScript является последним языком программирования. Было невыносимо. Мы должны сделать мир лучше, по крайней мере для наших детей, не так ли?
Я думал о том, каким будет этот язык. Какие у него должны быть свойства? Какие проблемы он позволит решить? Как мы его распознаем, когда он все-таки появится?
В одной вещи я уверен: когда он, наконец, появится, мы отвергнем его. И причина этого в том, что программисты — эмоциональные и иррациональные ненормальные люди.
Мы считаем, что это не так. Мы думаем, что являемся ультра-рациональными, потому что исполняем роль послов людей в мире компьютеров, а компьютеры вполне рациональны. Соответственно, мы предполагаем, что и сами также рациональны. Многие из нас лишились социальных навыков, но выходит так, что иррациональность и эмоциональность никуда не делась. Давайте посмотрим на доказательства в поддержку этого тезиса:
Причина, почему все происходит так долго, в том, что мы не можем изменить ум. Мы должны ждать, пока поколение уйдет на пенсию или умрет, прежде чем мы сможем получить критическую массу пользователей новой хорошей идеи и продолжать работать уже с ней.
Я помню, когда появился go to. Спор вокруг него не утихал годами, а потом вдруг стало тихо. Можем ли мы избавиться от него сейчас? Да, мы уже просто бросили его. Разве кто-нибудь страдает от отсутствия go to? Все споры оказались бессмысленным, произошел сдвиг парадигмы.
Людям действительно сложно сдвинуть эту парадигму. История с go to — это просто избавление от одной конструкции и изменение способа, структурирования программы. Оказалось, что отсутствие go to облегчает программирование.
Именно поэтому я считаю, что следующий язык программирования в первую очередь будет отклонен.
Подумайте о том, как мы классифицируем языки программирования. Я делю языки на два основных набора: системные и прикладные.
Системные языки используются для написания распределителей памяти, системных ядер, драйверов устройств — это языки очень низкого уровня. Все остальное должно быть написано на прикладных языках.
Возможно, самая большая проблема Java заключается в том, что ее создатели никак не могли решить, на какой стороне этой деления они хотели быть. И поэтому Java пытается оседлать обе задачи.
Нам нужны новые языки в обеих категориях. Например, доминирующим системным языком на сегодняшний день остается C, появившийся в шестидесятые годы. Но моя работа сосредоточена на прикладных языках — именно с ними большинство из нас и будут иметь дело.
Прикладные языки также можно разделить на две группы: классическую школу (к которой относятся почти все языки) и группу языков для прототипного программирования, которая включает JavaScript.
И я думаю, JavaScript правильно выбрал направление. Вызывая много критики, JavaScript содержит инновации.
Когда вы программируете на языке классической школы, проектируя систему, вам необходимо сделать классификацию объектов. Нужно проанализировать все объекты в вашей системе, чтобы понять, из чего они состоят, провести таксономию, выяснить, как классы будут связаны друг с другом, как и что будет реализовано. Все это довольно сложно, поскольку делается чаще всего в начале проекта, т.е. в той точке, где вы имеете минимальное понимание о том, как система должна будет работать. Поэтому неизменно вы получаете неправильную таксономию. И поэтому вы в конечном итоге получаете неправильно выделенные объекты. И с этого момента вы боретесь с ними. Все это не позволяет решить задачу правильно. Вы хотите, чтобы у вас было множественное наследование, но все это просто не работает. Более того, вы обнаруживаете, что поломка работает на новом, более высоком уровне, поскольку все, что наследуется от первого набора объектов, также неверно. В итоге вы получаете структурные проблемы по всей системе. И в конечном итоге становится так плохо, что вам приходится прибегать к реорганизации, чего делать очень не хочется, поскольку вам приходится разрывать все на части и собрать вместе заново. И вы надеетесь, что все это соберется вместе (а может и не собраться).
Все это не является обязательной частью объектно-ориентированного программирования. Это просто классы. Если вы займетесь объектно-ориентированным программированием без классов, вам не придется ничего делать. Поэтому в JavaScript, если вы просто делаете прототипы (и делаете их правильно), вам не нужно делать таксономию и выполнять рефакторинг. Это намного проще.
Я постоянно пытаюсь объяснить это Java-разработчикам. Но они так и не осознают, что таксономию делать не надо. Здесь нужен сдвиг парадигмы — они просто не могут осознать, что таксономия делает их несчастными, не могут представить другого способа.
Раньше я был сторонником прототипического наследования. Его основное преимущество заключается в сохранении памяти. В этом случае мы копируем объект, запоминая только реальное различие между оригиналом и копией, т.е. памяти выделяем меньше. И, возможно, это было самым важным фактором в 1995 году. Но не сегодня. Объем доступной памяти постоянно растет в соответствии с Законом Мура. Теперь у вас есть гигабайты оперативной памяти буквально в вашем кармане. Поэтому беспокоиться о том, сколько битов выделяется на каждый объект сегодня — просто потеря времени. Но мы по-прежнему делаем это.
Выгода, которую мы получаем от прототипов, на деле не является преимуществом. Кроме того, есть определенные расходы. К примеру, собственные свойства отличаются от унаследованных. И это различие вызывает путаницу, что является источником ошибок. Мы получаем ретроактивную наследственность, когда вы можете изменить наследуемый объект после его создания. Пользы в этом никакой, но я могу представить много способов использовать это во вред.
Все это также снижает производительность. Современные движки JavaScript работают очень быстро, делая предположения о параметрах объектов. Но они вынуждены с пессимизмом относиться к цепочкам прототипов, поскольку прототип может быть в любое время изменен. Таким образом, наличие такого уровня косвенности в языке фактически замедляет процесс.
Поэтому, хотя я и был сторонником прототипического наследования, теперь я придерживаюсь бесклассового объектно-ориентированного программирования. Я думаю, такой подход — дар JavaScript человечеству.
Позвольте объяснить, что я имею в виду. Это пример области видимости блока. Вы можете создать внутренний блок, который видит переменные внешнего блока. При этом другой блок не может видеть переменные во внутреннем блоке.
JavaScript всегда был в состоянии так работать с функциями, поскольку функция — это просто блок, связанный с объектом. И поэтому функции имеют те же взаимоотношения.
Множество переменных, которые видит внешняя функция, включается в множество переменных, видимых для внутренней функции (поэтому мы такие взаимоотношения называем замкнутыми — это понятие из теории множеств).
Но что делать, если внутренняя функция живет дольше, чем внешняя? В этом случае при вызове внешней функции в стеке выделяется А. Когда внешняя функция возвращает значение, А из стека удаляется. Как сделать, чтобы теперь внутренняя функция могла использовать это A?
Понадобилась пара поколений, чтобы понять, как это сделать. Это тривиально — просто не использовать стек, а выделять место в heap, имея при этом хороший сборщик мусора. Именно так мы это делаем сейчас.
С учетом всего этого вот, как я создаю объекты, в соответствии с ES6. Итак, у меня есть функция constructor:
Здесь я задаю спецификацию объекта. И мне это нравится гораздо больше, чем задание некого количества переменных.
Несколько лет назад я спроектировал ужасный конструктор, в котором было множество параметров, и никто не мог вспомнить, в каком порядке они шли. В какой-то момент мы осознали, что никто никогда не использует третий параметр. Но мы не могли ничего изменить, и это было ужасно. Вместо этого я создал один объект, описывающий то, что мы пытаемся сделать. Получилось более гибко — мы могли иметь значения по умолчанию, а также добавлять или удалять вещи со временем.
Но вернемся к нашему примеру. Я использую деструктуризацию, чтобы получить значения из объекта и инициализировать локальные переменные.
Далее вызываю другой конструктор. Я могу передать ту же спецификацию объекта, и это будет преобразование объекта. Затем я беру методы и помещаю их в локальные переменные. Я могу повторять вызов столько раз, сколько захочу, так что если мне необходимо множественное наследование или что-то вроде того, я могу реализовать это очень легко. Затем я создаю свои методы.
После этого я выполняю return. В ES6 у нас есть еще одна сокращенная нотация: вместо вызова метода можно просто написать method. Что приятно, это больше похоже на объявление, чем на литерал.
Далее я собираюсь его заморозить. Теперь это объект, который нельзя изменить, что правильно определяет его локальное состояние. Это лучший способ реализовать подобное на JavaScript. И я думаю, что это действительно важное свойство для объектов. В начале объектно-ориентированного программирования мы стартовали с идеи, реализованной в Pascal, согласно которой у нас есть запись, а затем мы связываем с этой записью функции, которые являются методами, действующими на элементы записи. И ранее я не мог выйти за эти рамки. Теперь же у меня теперь есть два совершенно разных набора объектов. У меня есть замороженные объекты, которые содержат только функции, и у меня есть значимые объекты, которые просто содержат данные. И я не смешиваю их. Я использую их друг с другом, чтобы защитить данные и обеспечить соблюдение дисциплины.
У меня есть еще немного времени, чтобы рассказать вам историю одной ошибки. В 2001 году я писал на Java один из первых парсеров JSON. И он содержал конструкцию this. Я создавал локальную переменную с именем index, она была int. Переменная измеряла, сколько символов было в файле или потоке, без парсинга. При обнаружении синтаксической ошибки переменная могла сообщить вам, в каком именно месте она произошла ошибка.
Напомню, я написал это в 2001 году. Я хорошо подумал, что int дает мне примерно 2 миллиарда байт — это два гигабайта. В то время это был довольно большой диск. Я не мог себе представить, что текст JSON окажется больше. Самый большой текстовый файл из тех, что я создавал, был пару килобайт.
Я думал, что все хорошо, пока в прошлом году не получил сообщение об ошибке от кого-то, у кого текст JSON был 7 Gb. И он содержал синтаксическую ошибку за пределами первых двух миллиардов символов, поэтому переменная давала в корне неверное значение. А все потому, что я выбрал int. Это оказалось ошибкой.
Я ненавижу int. У int есть ужасное свойство — он переполняется без предупреждения. Что должно произойти, если у вас есть число, которое слишком велико для помещения в int? Считается, что программа не должна вылетать. В итоге значимые биты выкидываются, а вы получаете неверные результаты. Лучше было бы бросить эксепшн, который скажет, что «что-то не так».
Альтернативный вариант — замена: когда вы попробуете получить значение, вам сообщат, что его больше здесь нет. Но существующий вариант — худшее, что можно было придумать; это путь максимизировать ошибки. Более того, эти ошибки никогда не выявляются в тестах, поскольку тесты изначально предполагают, что данные соответствуют диапазонам.
Может случиться так, что система будет работать в течение 10 или 20 лет, прежде чем ошибка будет выявлена. Я не хочу, чтобы моя система вообще когда-либо давала сбой, поэтому приглашаю вас с этим разобраться.
Причина всех проблем с типами данных родом из пятидесятых годов, когда компьютеры были ламповыми. Чем больше ламп, тем больше требуется энергии и косвенных затрат (например, тем быстрее выгорают лампы).
Кроме того, задействовать дополнительную память было действительно страшно. У машины могла быть только пара килобайт. К примеру, память Atari 2600 была всего 128 байтов. И поэтому не хотелось выделять 64 бита на то, что могло бы быть представлено 4 битами.
И кто-то догадался в целях экономии использовать дополнительную арифметику. Догадка была действительно великолепной. Но, спустя 50 лет, мы должны спросить себя — почему мы все еще делаем это?
Аналогичная ситуация сохраняется на других языках. Например, в Java у вас есть byte, char, short, int, long, flot, double и т.п. Каждый раз, когда вам нужно создать параметр, переменную или элемент, вы должны спросить себя, к какому типу будет относится новая структура. Значение сэкономленной при этом памяти настолько мало, что говорить о нем нет смысла. Нет никакой пользы в том, чтобы сделать правильный выбор. Фактически время, которое вы потратили на размышления, стоит бесконечно больше, чем сэкономленный ресурс. Но если вы ошибетесь, все перестанет работать, и тесты это не выявят. Это плохо.
С другой стороны, JavaScript имеет только один численный тип. Здесь вы не можете ошибиться. Единственная проблема с JavaScript заключается в том, что это неправильный тип. При его использовании 0.1 + 0.2 не равно 0.3. Это фундаментальная вещь. Проблема в двоичной плавающей запятой — это снова нечто, что имело смысл в пятидесятые годы.
В сороковые годы, когда проектировалась первая машина, речь шла о математике с целыми числами. Приходилось работать со шкалой арифметики, чтобы получить реальные значения. И это было тяжело. Но кто-то предложил использовать второе значение, связанное с каждым числом, которое сообщает, где находится десятичная точка. И это было намного проще. К сожалению, работало это очень медленно, поэтому работу с плавающей точкой перенесли на уровень аппаратного обеспечения.
В пятидесятые годы какой-то блестящий инженер пришел к мысли, что можно использовать двоичную плавающую точку вместо десятичной: вместо того, чтобы делать сдвиг на 10 при помощи деления, мы можем сделать сдвиг на один бит, что намного дешевле.
По сей день мы продолжаем это делать. И это неправильно, поскольку вызывает проблемы. Это было так же неправильно и в пятидесятые годы: бизнесмены возражали, что не могут пользоваться такими вычислениями. Вместо этого они использовали BCD (двоично-десятичный код). Таким образом языки программирования разделились. Вы могли использовать Fortran с плавающей запятой или COBOL с BCD. В конце концов, ситуация стала совсем запутанной. Java стала наследником COBOL. Но Java не была предназначена для ведения бизнес-процессов. Так что этот функционал здесь просто не используется.
Я предлагаю решение этой ужасной проблемы. Я назвал его DEC64. Это 64-битное значение, которое на самом деле очень похоже на то, что было сделано в сороковых годах.
Здесь используется 56-битный коэффициент, который является просто большим целым числом, а также отрицательный показатель. На Intel архитектуре я могу распаковать это практически бесплатно. С точки зрения научных вычислений этот формат также делает все, что нужно. Он соответствует и нуждам бизнеса. Поэтому я предлагаю его как единственный численный тип в будущих прикладных языках (у системных языков другие потребности, и поэтому они будут делать другие вещи).
Я написал реализацию DEC64 на ассемблере Intel x64 — используйте ее, если хотите опробовать тип в аппаратной реализации (dec64.com, GitHub).
Этот тип избавит от необходимости иметь отдельный int. Кроме того, он правильно считает деньги и выполняет арифметические операции так, как нас учили это делать в средней школе.
Мое предложение сделать этот тип единственным означает, что я должен убедить всех в этом мире, что нужно делать именно так. И я догадываюсь, насколько это сложно. Но на самом деле мне не нужно убеждать всех в этом мире. Мне нужно убедить только одного человека — мужчину или женщину, который(которая) разрабатывают следующий язык программирования. Если я смогу убедить этого человека, данный тип, который хорошо работает для людей, станет единственным.
У меня есть немного опыта в сфере убеждения всего мира в том, как поступать правильно. Пример — мой любимый формат обмена данными JSON.
Это статистика Google, которая показывает относительную популярность XML (красным) и JSON (синим). Вы можете видеть, что мир неуклонно теряет интерес к XML с 2005 года, и он очень медленно растет интерес к JSON.
Стоит отметить, что на рост популярности JSON не повлияла никакая отрасль. Нет крупных корпораций, которые продают большие инструменты JSON, потому что вам не нужны большие инструменты для работы с JSON. Вот почему вам он нравится. Это просто работает.
Похоже, скоро будет пересечение. Я не могу предсказать, когда это произойдет. Но Google может. Согласно прогнозам Google Trends это произойдет в этом году (2015 — прим. ред.). JSON наконец станет официально более интересным, чем XML.
Если вам интересна оригинальная видеозапись доклада на английском, с радостью ее предоставляем:
На грядущем HolyJS Piter вы сможете прослушать два доклада Дугласа Крокфорда:
Кроме них в течение двух дней вы услышите еще более 20 докладов о JavaScript: от новых фронтенд фреймворков до разбора серверного перфоманса и десктопной разработки. Будут доклады от Lea Verou, Martin Splitt, Anjana Vakil, Claudia Hernández и много кого еще. Детали смотрите на сайте конференции.
Путь к совершенству
Этот человек — известный авиатор и писатель Антуан де Сент-Экзюпери. Большинство его помнит за то, что он является автором замечательной детской книги «Маленький принц» (хотя книга эта не совсем детская). Помимо нее он написал много других книг, и в одной из них есть удивительная фраза: «Как видно, совершенство достигается не тогда, когда уже нечего прибавить, но когда уже ничего нельзя отнять».
Это блестящая цитата. Она использовалась в разговорах о дизайне, архитектуре, притягивалась ко всему, что сочетает в себе креативность и дисциплину.
Он говорил о конструкции самолетов. Но, кажется, мысль на самом деле шире. Я думаю, что она лучшим образом подходит к компьютерной программе. Поскольку у нас особые отношения с совершенством, отсутствующие в других дисциплинах: то, что мы пишем, должно быть совершенным, или оно будет вести себя неправильно. И автор дает нам некоторое представление о том, как мы достигаем совершенства — через вычитание.
Я думаю, что это относится и к языкам программирования.
Языки программирования имеют тенденцию к постепенному усложнению. Но если мы хотим достичь совершенства, необходимо удалить оттуда некоторые вещи. В этом и заключается принцип сильных сторон. Согласно этому принципу, если функция в одних случаях полезна, а в других — опасна, при этом есть лучший вариант, всегда используйте лучший вариант.
Это удивительно противоречивое заявление. Есть множество людей, не желающих использовать лучший вариант. И происходит это из-за непонимания одной вещи: нам не платят за использование каждой функции языка. В конце проекта нет менеджера с портфелем, проверяющего: «А использовали ли вы двойное равенство»? Никто не заботится об этом. Нам платят за то, чтобы мы писали программы, которые хорошо работают и не содержат ошибок.
Когда «отсутствие ошибок» стало частью сделки? Оно всегда была ее частью. Просто мы очень редко достигаем этого. Легко забыть, что на самом деле отсутствие ошибок было первым требованием. Поэтому хороший язык программирования должен научить вас этому.
Я призываю людей изучать как можно больше языков программирования, потому что каждый из них даст вам новые идеи, которые вы сможете применить к другим языкам.
Язык, который научил меня больше всего, — это JavaScript. У меня было много времени, чтобы разобраться с ним, потому что я допустил каждую ошибку, которую только можно допустить в JavaScript. И начал с самой первой — наихудшей — я не удосужился выучить язык до того, как начал писать в нем.
В конце концов я изучил язык, а он научил меня. И продолжает меня учить. Я использовал этот язык для написания инструмента под названием JSLint, который читает программы JavaScript и подсказывает, как сделать их лучше. И JSLint научил меня еще больше. Все это изменило мою точку зрения на программирование, так что теперь моя основная цель — пытаться создавать программы, которые не содержат ошибок. И JSLint дал мне много информации о том, как это делать.
Я написал книгу о своем опыте использования JavaScript и кода JSLint — «JavaScript — сильные стороны». Возможно, вы слышали о ней. Она по-прежнему бестселлер, что редкость для книг по программному обеспечению. Большинство книг по программному обеспечению устаревают еще до выпуска, а эта по-прежнему актуальна. Все потому, что «сильные стороны» по-прежнему остаются сильными сторонами. Что хорошо, язык не изменился вообще.
Контраргументы
Теперь аргументы против использования «сильных сторон». Я бы хотел изложить их для вас:
- Первый звучит так: «что хорошо, а что плохо — это просто вопрос мнения». Это неправда. В рамках поддержки JSLint я получаю сообщения об ошибках от людей со всего мира. Недавно я получил письмо от одной компании, которая провела две недели, пытаясь решить некую проблему. Выяснилось, что в одном месте была случайно добавлена точка перед знаком равенства. Никто не видел ее, потому что она выглядела корректно, но результат был плохим. Поэтому они спросили меня, могу ли я модифицировать JSLint, чтобы никто больше не страдал от подобной проблемы. Почему нет? Я делаю это уже много лет. Так что если вы используете JSLint, вы никогда не потратите впустую две недели на проблему такого рода. И это не мнение, это факт.
- Каждая функция является важным инструментом. Это неправда. Вы можете написать лучшие программы, не используя некоторые из этих функций. И если вы можете писать более качественные программы без них, то они не так уж важны.
- Я имею право использовать каждую функцию. Тема разговора изменилась. Неужели мы больше не говорим о том, как лучше всего писать программы. Мы сейчас говорим о наших правах? На этом аргументация, в конечном итоге, заканчивается. «Я имею право писать дерьмо», правда? Это не важно. Важно, что мы обязаны писать очень хорошо.
- Мне нужна свобода самовыражения или «я художник и я самовыражаюсь, ставя точку с запятой в начале оператора, а не в конце». Соображения те же.
- Мне нужно уменьшить количество нажатий клавиш. Мы предполагаем, что большую часть времени печатаем. И если бы мы могли найти способ компоновки программ с помощью нескольких нажатий клавиш, это сделало бы нас намного эффективнее. Но дело обстоит с точностью до наоборот. Главный растратчик времени — не набор символов, а взгляд в бездну с возгласом: «мой бог, что я сделал, почему это не работает». Вот куда мы тратим большую часть времени. Если бы я мог предложить вам схему, согласно которой увеличение количества нажатий клавиш в 10 раз могло бы сократить ошибки вдвое, это было бы огромной победой (к сожалению, у меня нет такой схемы).
- «Это оскорбление предполагать, что я когда-либо ошибусь, используя опасную функцию». «Я знаю, что можно ошибиться, но я настолько искусен, что это спасает меня. Думать иначе — оскорбление для меня, личная травма». Тут все понятно.
- «Есть причина, почему эти функции были добавлены в язык». Это абсолютно неверно. Существует множество причин, почему вещи попадают в язык, но большинство из них не являются вескими.
Например, JavaScript был разработан и реализован всего за 10 дней. Это удивительно. И блестящий человек, который это сделал, Брендан Айк, за эти 10 дней совершил несколько ошибок. Одна из них — оператор двойного равенства, который он заставил делать преобразование типа, прежде чем сравнивать элементы. Это вызывает ложные срабатывания, которые путают. Брендан Айк признал, что оператор был реализован неправильно. В то время многие другие языки (например, PHP) имели ту же ошибку. Брендан хотел исправить это. Поэтому, когда началась работа над стандартом, он решил, что сейчас самое время внести исправления. Он пошел к ECMA и сказал: «Такое поведение неверно, давайте сделаем правильно». Но в ECMA отказали, хотя и предложили компромисс: ввести оператор тройного равенства, который будет работать правильно. При этом двойное равенство осталось в языке для людей, которые уже начали использовать неправильную конструкцию. В итоге мы имеем оператор без веских на то оснований.
Брендан называет подобные особенности «выстрелом в ногу» (footgun — в дословном переводе пистолет для выстрела в ногу, — прим. ред.). И он непреднамеренно поместил их в JavaScript. Их использовать не рекомендуется.
Итак, целью языков программирования является помощь разработчикам в создании программ, не содержащих ошибок.
Раньше мы думали, что писать хорошие программы на JavaScript просто невозможно, потому что это был такой хрупкий язык. Но оказывается, что писать хорошие программы на JavaScript не только невозможно, но и не нужно. Как раз из-за своей «хрупкости» JavaScript требует большей дисциплины, чем любой другой язык программирования. И вы действительно должны ее придерживаться, чтобы писать нечто, что будет работать.
Есть две вещи, которые работают против этого. Во-первых, это фантазия о непогрешимости. В особенности молодые программисты считают, что их навыки настолько развиты, что они могут делать удивительные вещи, которые будут работать. Во-вторых, есть бесполезность безупречности, которую вы видите особенно у старых парней, которые делали годами то, что никогда не работало. Эти два очень разных взгляда приводят к одному и тому же: danger driven development. И это плохо.
Одна из вещей, усложняющих управление разработкой программного обеспечения, — это сложность планирования. Есть два времени, о которых вы должны знать. Время A — это время, необходимое для написания кода. Мы действительно плохо оцениваем его — у нас нет науки, которая ответила бы на вопрос, как оценивать время A. Но еще хуже обстоит дело с оценкой времени B — времени, необходимого на то, чтобы заставить код работать правильно.
Время B должно быть равно 0, верно? Вы пишете код — он должен работать. Но время В иногда становится больше времени А. Иногда оно бесконечно. Подобное происходит, когда у вас есть проект по разработке ПО, который заканчивается, но затем от ПО отказываются, прежде чем оно начинает работать.
Все, что вы делаете в рамках времени A, увеличивающее время B, неправильно. Вы должны пытаться сократить время B до нуля.
Новые сильные стороны ES6
Есть ряд дополнений к JavaScript, которые будут отправлены на ECMA General Assembly в июне этого года (эти нововведения действительно прошли через процедуру сертификации и стали частью новой спецификации — ECMAScript 2015 Language Specification, — прим. ред.).
Рад сообщить, что там есть некоторые новые сильные стороны. И сегодня я бы хотел поделиться ими с вами.
Хвостовая рекурсия
Первое и лучшее — это конструкция, называемая хвостовой рекурсией. Если последнее, что делает функция, возвращает результат вызова функции (которая может быть вызовом другой функции), вместо создания обратной последовательности вызова компилятор генерирует скачок. Так что код будет работать немного быстрее, что приятно. Но что еще лучше — для некоторых моделей это значительно уменьшит потребление памяти. Таким образом, можно использовать совершенно новый класс алгоритмов.
Например, мы можем использовать стиль передачи продолжений и другие виды программирования, которые мы не могли реализовать на старом языке. Так что с этой функцией JavaScript, наконец, становится реальным функциональным языком программирования, и это здорово.
Многоточие
Также мы получили оператор многоточия. Если мы поместим его в список параметров или аргументов, сможем иметь дело с переменным количеством аргументов, что действительно приятно.
Здесь у нас есть две версии функции curry. Первая — в соответствии с ES6:
function curry(func, ...first) {
return function (...second) {
return func(...first, ...second);
};
}
Вторая — как это реализовывалось ранее:
function curry(func) {
var slice = Array.prototype.slice, args = slice.call(arguments, 1);
return function () {
return func.apply(null, args.concat(slice.call(arguments, 0)));
};
}
Я не буду объяснять, что именно происходит во второй версии, потому что все это просто ужасно. Первая, наоборот, выглядит довольно разумно: всюду мы видим точки (здесь у вас может быть столько аргументов, сколько вы захотите). И это замечательно.
Нововведение не позволяет нам делать то, что мы не могли делать ранее. Но когда приходится иметь дело с переменным числом аргументов, такой подход намного приятнее.
Модули
Теперь у нас появились модули. Вы можете импортировать и экспортировать значения из разных файлов; и это наконец-то имеет поддержку в языке. Раньше все это реализовывалось через глобальные переменные, и это было ужасно.
Теперь мы можем сделать это правильно. Если вам не кажется, что это здорово, попробуйте реализовать то же самое в соответствии с предыдущим стандартом. Но с ES6 мы наконец можем делать это правильно на полностью асинхронном языке, без блокировок.
Константы
У нас появилось два новых способа определения переменных: let и const, что решает проблему области видимости блока. Оказывается, в хорошей программе вам не нужна область видимости блока. Но синтаксис JavaScript ранее выглядел так, будто область видимости блока есть (с его синтаксической конструкцией var), хотя это было не так, что смущало людей. Всякий раз из-за путанницы возникали ошибки. Let и Const позволяют этого избежать.
Опять же, новые конструкции не позволяют писать какие-либо программы, которые мы не могли написать раньше. Но теперь код не смущает Java-программистов, и это хорошо.
Деструктуризация
У нас появилась деструктуризация, которая представляет собой еще одну синтаксическую конфетку. Она также не позволяет вам делать то, что вы не могли сделать раньше. Но для некоторых вещей новый синтаксис намного более выразителен.
Например, здесь у меня есть объект. И я хочу создать некоторые переменные, инициализировав эти переменные из свойств объекта.
let {that, other} = some_object;
let that = some_object.that, other = some_object.other;
Это очень упрощенный пример. Позже я покажу вам еще один пример того, как вы могли бы использовать это.
WeakMaps
У нас появился WeakMap. WeakMap работает так, как должны были бы работать объекты. В объектах JavaScript ключи — это строки, что является ошибкой. Было бы лучше, если бы вместо них использовались какие-то значения. Но, тем не менее, это строки. WeakMap решает эту проблему. Здесь можно взять любое значение и использовать его в качестве ключа.
К сожалению, нам пришлось добавить эту вещь, и она значительно усложнила язык. Кроме того, мы назвали его худшим именем из когда-либо использованных в языках программирования (weak — слабый, англ.). Никто не захочет помещать в свою программу что-то слабое. Но работает эта конструкция действительно хорошо.
С помощью WeakMap вы можете писать программы, которые невозможно было создать на языке ранее.
Шаблонные строки
И наконец мы нечто, что я назвал мегастроковым литералом (в языке это называется шаблонными строками, но мне не нравится это имя; но ранее их называли квази-литералами, что еще больше сбивало с толку).
Это регулярные выражения, которое соответствует нескольким строкам в ES6. Объявление этого регулярного выражения просто ужасно; я надеюсь, когда-нибудь мы сделаем что-нибудь получше. Но в ES6 у нас нет лучшей альтернативы.
Итак, у нас есть функция, которая принимает строку и преобразовывает ее в регулярное выражение, предварительно удалив все пробелы. Имея такую функцию, я могу взять любое выражение, но не писать все в кучу, а поместить в него необходимое пустое пространство, чтобы видеть элементы и то, как они соотносятся друг с другом. Результат будет тем же самым.
У этой конструкции есть один недостаток — компилятор рассматривает регулярное выражение как строку, поэтому не может выполнить проверку. Валидация не произойдет, пока мы не вызовем конструктор. Но в предыдущей версии записи из-за того, что все было смешано в кучу, серьезные ошибки также могли легко проскочить мимо компилятора. Поэтому я не думаю, что мы потеряем много в этом преобразовании.
Кстати, если вы часто работаете с регулярными выражениями, я настоятельно рекомендую инструмент под названием RegulEx. Поместите в него регулярное выражение, и он построит его диаграмму, чтобы вы точно видели, что именно происходит внутри. Я использую его каждый раз, когда я пишу регулярные выражения.
Стрелочные функции
Еще одна новая функция, которую мы получили а ES6, это анонимные стрелочные функции. Хорошей мотивацией для их появления стало то, что некоторые люди жаловались на большой объем текста при наборе названий функций. Поэтому я добавил такую вещь. По сути это более короткое обозначение для функции. В результате я получаю функцию, которая будет принимать аргумент и возвращать объект, у которого указанное свойство имеет это значение.
Все хорошо, кроме того, что этого не работает должным образом. Если вы вызовете эту функцию, она вернет undefined вместо значения, потому что здесь присутствует ошибка. Так что это еще одна из тех маргинальных вещей, которые иногда хороши, а иногда — нет. Было бы здорово, если бы это работало все время, но это не так.
Недостатки ES6
Помимо сильных сторон, у ES6 есть и откровенно плохие элементы.
Классы
Худшее нововведение — это классы. Класс был самой востребованной новой функцией, причем большая часть запросов поступала от Java-программистов, которые теперь должны писать на JavaScript и они действительно возмущены этим. Хочется писать на Java, но задачи и деньги есть в JavaScript, поэтому им приходится это делать. Переучиваться они не хотят, поэтому новый синтаксис упростит их жизнь.
Все бы ничего, но полагаясь на этот новый синтаксис, они никогда не поймут, как работает язык. И они никогда не поймут, как эффективно использовать этот язык, хотя будут думать, что понимают. Они так и продолжат писать, не зная, насколько они несчастны. Это ловушка.
Object.Create
Я пересматриваю то, что было написано в «Сильных сторонах», учитывая новые знания и необходимость отражения нововведений ES6. Когда я писал «сильные стороны», я рекомендовал вместо классов использовать object.create.
Фактически, именно мне удалось добавить object.create к языку, чтобы именно я мог его использовать. Получилось неплохо. Но как же я был удивлен, когда заметил, что прекратил использовать object.create. Я добавил его туда для себя, но даже я его не использую. И причина в том, что я прекратил использовать this.
This
Я перестал использовать this из-за того, что в 2007 году сделал проект под названием ADSafe. В то время существовало несколько исследовательских групп, таких как fbjs в Facebook, Caja Google, Web Sandbox в Microsoft, кроме того, был мой собственный проект — ADSafe и другие. Все мы пытались выяснить, как сделать JavaScript безопасным языком, чтобы можно было добавить сторонний код в приложение и быть уверенным, что он не породит проблем с безопасностью. И в JavaScript это оказалось очень сложной задачей.
Одна из вещей, которая затрудняет ее решение — this. Если у вас есть this в методе, оно привязывается к списку событий объекта (это хорошо и необходимо). Но если вы вызываете тот же метод в качестве функции, this привязывается к глобальному объекту, что полностью нарушает нашу безопасность. Как с этим справиться?
Большинство проектов справились с этой проблемой при помощи компилятора, который переводит JavaScript в JavaScript со множеством рантайм-проверок и взаимодействий, чтобы предотвратить проблемы с безопасностью.
Мой подход в ADSafe был намного проще. Я просто сделал это незаконным, побудив отказаться от программ, где это используется. Это работает. Единственная проблема в том, что отказаться приходится от львиной доли программ, потому что все использовали this.
Однако моя гипотеза заключалась в том, что если удалить this из языка, мы все равно останемся с функциональным языком программирования, которого достаточно для написания хороших программ. Чтобы проверить ее, я начал писать также (без this) в свободном от ограничений JavaScript. И я был очень удивлен, обнаружив, что стал писать лучшие программы с меньшими усилиями, не имея this. Очень мило. Что и говорить, если наличие this в языке само по себе вызывает сложности: общаясь на английском, я не могу заранее знать, имеется в виду конструкция языка или местоимение. Это довольно сложно.
Null / undefined
Я перестал использовать null. JavaScript имеет два минимальных значения — null и undefined. В некоторых языках считается, что у вас и одного не должно быть. JavaScript — это единственный язык, который у которого таких значений более одного. Но совершенно точно, что глупо иметь два сразу.
Некоторые фреймворки пытаются рассматривать их как взаимозаменяемые, но они таковыми не являются. В зависимости от ситуации я должен использовать только один из них. Я использовал undefined, потому что именно его использует сам язык: вы получаете undefined, если, к примеру, обращаетесь к отсутствующему свойству.
Неопределенное значение порождает целый круг проблем: совершенно неправильный тип отсутствующего объекта. Мы надеялись, что это будет исправлено в ES6, но этого не произошло. Наверное, так будет всегда. Но если вы не используете null, то не столкнетесь с этой проблемой. И я перестал использовать ошибочные значения, решив, что само их существование в JavaScript — плохая идея.
Изначально эта идея появилась из C, который использует 0 для представления no, false и другие аналогичные значения. JavaScript попытался сделать то же самое. Но это сбивает с толку.
For
Я перестал использовать for. В ES5 мы ввели новый набор методов работы с массивами — for each, map и другие, поэтому теперь я использую эти инструменты. Если у меня есть массив и мне нужно его обработать, я использую один из этих методов. И я могу объединить их вместе удобным образом. Это действительно выразительно.
For Each
Я не использую for each. В ES5 мы получили object .keys(object). Эта конструкция дает вам хороший массив строк. И он не включает в себя вещи из цепочки прототипов, поэтому вам не нужно фильтровать результаты. Очень приятно, что я могу взять массив и выполнить for each таким образом, это действительно выразительный способ.
Ранее я уже упоминал, что в ES6 появились правильная хвостовая рекурсия. И сейчас пора перестать использовать циклы вовсе (в частности, оператор while) — время использовать только рекурсивные функции.
Например, это рекурсивная функция. Вы вызываете эту функцию до тех пор, пока она не вернет undefined. Первая версия кода написана с использованием цикла while.
function repeat(func) {
while (func() !== undefined) {
}
}
Вторая версия написана с использованием хвостовой рекурсии.
function repeat(func) {
if (func() !== undefined) {
return repeat(func);
}
}
В ES6 эти два способа должны работать с одинаковой скоростью, потребляя ровно столько же памяти. Таким образом, у циклов перед рекурсией больше нет преимущества. Это здорово.
Язык следующего поколения
Я много думал о языке программирования следующего поколения. Каким он будет? Вероятно, это должен быть язык, которым мы заменим JavaScript, потому что было бы очень грустно, если бы оказалось, что JavaScript является последним языком программирования. Было невыносимо. Мы должны сделать мир лучше, по крайней мере для наших детей, не так ли?
Я думал о том, каким будет этот язык. Какие у него должны быть свойства? Какие проблемы он позволит решить? Как мы его распознаем, когда он все-таки появится?
В одной вещи я уверен: когда он, наконец, появится, мы отвергнем его. И причина этого в том, что программисты — эмоциональные и иррациональные ненормальные люди.
Мы считаем, что это не так. Мы думаем, что являемся ультра-рациональными, потому что исполняем роль послов людей в мире компьютеров, а компьютеры вполне рациональны. Соответственно, мы предполагаем, что и сами также рациональны. Многие из нас лишились социальных навыков, но выходит так, что иррациональность и эмоциональность никуда не делась. Давайте посмотрим на доказательства в поддержку этого тезиса:
- Потребовалось поколение, чтобы признать языки высокого уровня хорошей идеей. Когда среди множества ассемблерных языков появился Fortran, разработчики (на тот момент они все были ассемблерными программистами) отказались на него переходить. Потому что Fortran отнимал у них контроль взаимодействия с машиной, чего они не могли допустить. Переход сделал бы их жизнь намного лучше, но они отказались от него.
- Потребовалось целое поколение, чтобы согласиться с тем, что go to был плохой идеей. Десятилетиями мы вели эмоциональные споры о том, следует ли нам использовать go to или нет. И приводились как глубоко продуманные аргументы от очень умных людей, так и заявления в стиле: «Это мой способ самовыражения», «Мне нужна эффективность», «Умру, но не отдам». Оказалось, что все эти доводы были совершенно неправильными.
- Потребовалось поколение, чтобы согласиться, что объекты были хорошей идеей. В 1967 году в Норвегии появился язык, названный Simula. Насколько я могу судить, только один человек в мире — Алана Кей из Университета штата Юта (США) — признал, что этот язык содержал важные идеи. Все остальные пропустили это. Он думал, что объекты, которые появились в Simula были настолько выразительными, что он мог бы разработать язык программирования для детей. И дети могли бы писать удивительные программы, используя эту парадигму. Поэтому он начал изучать этот язык и потратил почти десять лет на разработку и совершенствование собственного. В 1980 он наконец был опубликован. И это был лучший из разработанных языков программирования в истории — С++. Алан Кей взял идеи Simula, не совсем понял их, но перевел на C.
Итак, у индустрии появился выбор: мы могли пойти вслед за Simula или за C++. Итоговое решение принимали люди, которые в корне не понимали, чем объекты были для программирования. Но они сделали выбор в пользу C ++, потому что там вам не нужно было разбираться в объектно-ориентированном программировании, чтобы начать работу с языком. И с тех пор почти во всех языках больше заимствовано от C++, нежели от Simula. И поэтому мы до сих пор ошибаемся.
- И, наконец, потребовалось два поколения, чтобы согласиться с тем, что лямбды были хорошей идеей. Лямбды появились в языке под названием Squeak в MIT в начале семидесятых. И индустрия вообще не обратила на это внимания. Потребовалось даже не два, а четыре поколения, чтобы наконец лямбды добрались до мейнстрима. На это потребовалось так много времени, что некоторые считали это доказательством несостоятельности идеи. Но оказалось, что функциональное программирование с лямбдами очень эффективно при работе с асинхронностью и распределенными системами, с которыми мы имеем дело сегодня. Первый язык, принявший эту идею, — JavaScript. После этого лямбды появились в Python и Ruby, и, в конечном итоге, C#. Не так давно наконец в Java. Но JavaScript был первым.
Причина, почему все происходит так долго, в том, что мы не можем изменить ум. Мы должны ждать, пока поколение уйдет на пенсию или умрет, прежде чем мы сможем получить критическую массу пользователей новой хорошей идеи и продолжать работать уже с ней.
Я помню, когда появился go to. Спор вокруг него не утихал годами, а потом вдруг стало тихо. Можем ли мы избавиться от него сейчас? Да, мы уже просто бросили его. Разве кто-нибудь страдает от отсутствия go to? Все споры оказались бессмысленным, произошел сдвиг парадигмы.
Людям действительно сложно сдвинуть эту парадигму. История с go to — это просто избавление от одной конструкции и изменение способа, структурирования программы. Оказалось, что отсутствие go to облегчает программирование.
Именно поэтому я считаю, что следующий язык программирования в первую очередь будет отклонен.
О классификации языков
Подумайте о том, как мы классифицируем языки программирования. Я делю языки на два основных набора: системные и прикладные.
Системные языки используются для написания распределителей памяти, системных ядер, драйверов устройств — это языки очень низкого уровня. Все остальное должно быть написано на прикладных языках.
Возможно, самая большая проблема Java заключается в том, что ее создатели никак не могли решить, на какой стороне этой деления они хотели быть. И поэтому Java пытается оседлать обе задачи.
Нам нужны новые языки в обеих категориях. Например, доминирующим системным языком на сегодняшний день остается C, появившийся в шестидесятые годы. Но моя работа сосредоточена на прикладных языках — именно с ними большинство из нас и будут иметь дело.
Прикладные языки также можно разделить на две группы: классическую школу (к которой относятся почти все языки) и группу языков для прототипного программирования, которая включает JavaScript.
И я думаю, JavaScript правильно выбрал направление. Вызывая много критики, JavaScript содержит инновации.
Когда вы программируете на языке классической школы, проектируя систему, вам необходимо сделать классификацию объектов. Нужно проанализировать все объекты в вашей системе, чтобы понять, из чего они состоят, провести таксономию, выяснить, как классы будут связаны друг с другом, как и что будет реализовано. Все это довольно сложно, поскольку делается чаще всего в начале проекта, т.е. в той точке, где вы имеете минимальное понимание о том, как система должна будет работать. Поэтому неизменно вы получаете неправильную таксономию. И поэтому вы в конечном итоге получаете неправильно выделенные объекты. И с этого момента вы боретесь с ними. Все это не позволяет решить задачу правильно. Вы хотите, чтобы у вас было множественное наследование, но все это просто не работает. Более того, вы обнаруживаете, что поломка работает на новом, более высоком уровне, поскольку все, что наследуется от первого набора объектов, также неверно. В итоге вы получаете структурные проблемы по всей системе. И в конечном итоге становится так плохо, что вам приходится прибегать к реорганизации, чего делать очень не хочется, поскольку вам приходится разрывать все на части и собрать вместе заново. И вы надеетесь, что все это соберется вместе (а может и не собраться).
Все это не является обязательной частью объектно-ориентированного программирования. Это просто классы. Если вы займетесь объектно-ориентированным программированием без классов, вам не придется ничего делать. Поэтому в JavaScript, если вы просто делаете прототипы (и делаете их правильно), вам не нужно делать таксономию и выполнять рефакторинг. Это намного проще.
Я постоянно пытаюсь объяснить это Java-разработчикам. Но они так и не осознают, что таксономию делать не надо. Здесь нужен сдвиг парадигмы — они просто не могут осознать, что таксономия делает их несчастными, не могут представить другого способа.
Раньше я был сторонником прототипического наследования. Его основное преимущество заключается в сохранении памяти. В этом случае мы копируем объект, запоминая только реальное различие между оригиналом и копией, т.е. памяти выделяем меньше. И, возможно, это было самым важным фактором в 1995 году. Но не сегодня. Объем доступной памяти постоянно растет в соответствии с Законом Мура. Теперь у вас есть гигабайты оперативной памяти буквально в вашем кармане. Поэтому беспокоиться о том, сколько битов выделяется на каждый объект сегодня — просто потеря времени. Но мы по-прежнему делаем это.
Выгода, которую мы получаем от прототипов, на деле не является преимуществом. Кроме того, есть определенные расходы. К примеру, собственные свойства отличаются от унаследованных. И это различие вызывает путаницу, что является источником ошибок. Мы получаем ретроактивную наследственность, когда вы можете изменить наследуемый объект после его создания. Пользы в этом никакой, но я могу представить много способов использовать это во вред.
Все это также снижает производительность. Современные движки JavaScript работают очень быстро, делая предположения о параметрах объектов. Но они вынуждены с пессимизмом относиться к цепочкам прототипов, поскольку прототип может быть в любое время изменен. Таким образом, наличие такого уровня косвенности в языке фактически замедляет процесс.
Поэтому, хотя я и был сторонником прототипического наследования, теперь я придерживаюсь бесклассового объектно-ориентированного программирования. Я думаю, такой подход — дар JavaScript человечеству.
Позвольте объяснить, что я имею в виду. Это пример области видимости блока. Вы можете создать внутренний блок, который видит переменные внешнего блока. При этом другой блок не может видеть переменные во внутреннем блоке.
{
let a;
{
let b;
... a …
... b …
}
... a ...
}
JavaScript всегда был в состоянии так работать с функциями, поскольку функция — это просто блок, связанный с объектом. И поэтому функции имеют те же взаимоотношения.
function green() {
let a;
function yellow() {
let b;
... a …
... b …
}
... a ...
}
Множество переменных, которые видит внешняя функция, включается в множество переменных, видимых для внутренней функции (поэтому мы такие взаимоотношения называем замкнутыми — это понятие из теории множеств).
Но что делать, если внутренняя функция живет дольше, чем внешняя? В этом случае при вызове внешней функции в стеке выделяется А. Когда внешняя функция возвращает значение, А из стека удаляется. Как сделать, чтобы теперь внутренняя функция могла использовать это A?
Понадобилась пара поколений, чтобы понять, как это сделать. Это тривиально — просто не использовать стек, а выделять место в heap, имея при этом хороший сборщик мусора. Именно так мы это делаем сейчас.
С учетом всего этого вот, как я создаю объекты, в соответствии с ES6. Итак, у меня есть функция constructor:
function constructor(spec) {
let {member} = spec,
{other} = other_constructor(spec),
method = function () {
// member, other, method
};
return Object.freeze({
method,
other
});
}
Здесь я задаю спецификацию объекта. И мне это нравится гораздо больше, чем задание некого количества переменных.
Несколько лет назад я спроектировал ужасный конструктор, в котором было множество параметров, и никто не мог вспомнить, в каком порядке они шли. В какой-то момент мы осознали, что никто никогда не использует третий параметр. Но мы не могли ничего изменить, и это было ужасно. Вместо этого я создал один объект, описывающий то, что мы пытаемся сделать. Получилось более гибко — мы могли иметь значения по умолчанию, а также добавлять или удалять вещи со временем.
Но вернемся к нашему примеру. Я использую деструктуризацию, чтобы получить значения из объекта и инициализировать локальные переменные.
Далее вызываю другой конструктор. Я могу передать ту же спецификацию объекта, и это будет преобразование объекта. Затем я беру методы и помещаю их в локальные переменные. Я могу повторять вызов столько раз, сколько захочу, так что если мне необходимо множественное наследование или что-то вроде того, я могу реализовать это очень легко. Затем я создаю свои методы.
После этого я выполняю return. В ES6 у нас есть еще одна сокращенная нотация: вместо вызова метода можно просто написать method. Что приятно, это больше похоже на объявление, чем на литерал.
Далее я собираюсь его заморозить. Теперь это объект, который нельзя изменить, что правильно определяет его локальное состояние. Это лучший способ реализовать подобное на JavaScript. И я думаю, что это действительно важное свойство для объектов. В начале объектно-ориентированного программирования мы стартовали с идеи, реализованной в Pascal, согласно которой у нас есть запись, а затем мы связываем с этой записью функции, которые являются методами, действующими на элементы записи. И ранее я не мог выйти за эти рамки. Теперь же у меня теперь есть два совершенно разных набора объектов. У меня есть замороженные объекты, которые содержат только функции, и у меня есть значимые объекты, которые просто содержат данные. И я не смешиваю их. Я использую их друг с другом, чтобы защитить данные и обеспечить соблюдение дисциплины.
Единственный численный тип
История одной ошибки
У меня есть еще немного времени, чтобы рассказать вам историю одной ошибки. В 2001 году я писал на Java один из первых парсеров JSON. И он содержал конструкцию this. Я создавал локальную переменную с именем index, она была int. Переменная измеряла, сколько символов было в файле или потоке, без парсинга. При обнаружении синтаксической ошибки переменная могла сообщить вам, в каком именно месте она произошла ошибка.
Напомню, я написал это в 2001 году. Я хорошо подумал, что int дает мне примерно 2 миллиарда байт — это два гигабайта. В то время это был довольно большой диск. Я не мог себе представить, что текст JSON окажется больше. Самый большой текстовый файл из тех, что я создавал, был пару килобайт.
Я думал, что все хорошо, пока в прошлом году не получил сообщение об ошибке от кого-то, у кого текст JSON был 7 Gb. И он содержал синтаксическую ошибку за пределами первых двух миллиардов символов, поэтому переменная давала в корне неверное значение. А все потому, что я выбрал int. Это оказалось ошибкой.
Я ненавижу int. У int есть ужасное свойство — он переполняется без предупреждения. Что должно произойти, если у вас есть число, которое слишком велико для помещения в int? Считается, что программа не должна вылетать. В итоге значимые биты выкидываются, а вы получаете неверные результаты. Лучше было бы бросить эксепшн, который скажет, что «что-то не так».
Альтернативный вариант — замена: когда вы попробуете получить значение, вам сообщат, что его больше здесь нет. Но существующий вариант — худшее, что можно было придумать; это путь максимизировать ошибки. Более того, эти ошибки никогда не выявляются в тестах, поскольку тесты изначально предполагают, что данные соответствуют диапазонам.
Может случиться так, что система будет работать в течение 10 или 20 лет, прежде чем ошибка будет выявлена. Я не хочу, чтобы моя система вообще когда-либо давала сбой, поэтому приглашаю вас с этим разобраться.
DEC64
Причина всех проблем с типами данных родом из пятидесятых годов, когда компьютеры были ламповыми. Чем больше ламп, тем больше требуется энергии и косвенных затрат (например, тем быстрее выгорают лампы).
Кроме того, задействовать дополнительную память было действительно страшно. У машины могла быть только пара килобайт. К примеру, память Atari 2600 была всего 128 байтов. И поэтому не хотелось выделять 64 бита на то, что могло бы быть представлено 4 битами.
И кто-то догадался в целях экономии использовать дополнительную арифметику. Догадка была действительно великолепной. Но, спустя 50 лет, мы должны спросить себя — почему мы все еще делаем это?
Аналогичная ситуация сохраняется на других языках. Например, в Java у вас есть byte, char, short, int, long, flot, double и т.п. Каждый раз, когда вам нужно создать параметр, переменную или элемент, вы должны спросить себя, к какому типу будет относится новая структура. Значение сэкономленной при этом памяти настолько мало, что говорить о нем нет смысла. Нет никакой пользы в том, чтобы сделать правильный выбор. Фактически время, которое вы потратили на размышления, стоит бесконечно больше, чем сэкономленный ресурс. Но если вы ошибетесь, все перестанет работать, и тесты это не выявят. Это плохо.
С другой стороны, JavaScript имеет только один численный тип. Здесь вы не можете ошибиться. Единственная проблема с JavaScript заключается в том, что это неправильный тип. При его использовании 0.1 + 0.2 не равно 0.3. Это фундаментальная вещь. Проблема в двоичной плавающей запятой — это снова нечто, что имело смысл в пятидесятые годы.
В сороковые годы, когда проектировалась первая машина, речь шла о математике с целыми числами. Приходилось работать со шкалой арифметики, чтобы получить реальные значения. И это было тяжело. Но кто-то предложил использовать второе значение, связанное с каждым числом, которое сообщает, где находится десятичная точка. И это было намного проще. К сожалению, работало это очень медленно, поэтому работу с плавающей точкой перенесли на уровень аппаратного обеспечения.
В пятидесятые годы какой-то блестящий инженер пришел к мысли, что можно использовать двоичную плавающую точку вместо десятичной: вместо того, чтобы делать сдвиг на 10 при помощи деления, мы можем сделать сдвиг на один бит, что намного дешевле.
По сей день мы продолжаем это делать. И это неправильно, поскольку вызывает проблемы. Это было так же неправильно и в пятидесятые годы: бизнесмены возражали, что не могут пользоваться такими вычислениями. Вместо этого они использовали BCD (двоично-десятичный код). Таким образом языки программирования разделились. Вы могли использовать Fortran с плавающей запятой или COBOL с BCD. В конце концов, ситуация стала совсем запутанной. Java стала наследником COBOL. Но Java не была предназначена для ведения бизнес-процессов. Так что этот функционал здесь просто не используется.
Я предлагаю решение этой ужасной проблемы. Я назвал его DEC64. Это 64-битное значение, которое на самом деле очень похоже на то, что было сделано в сороковых годах.
Здесь используется 56-битный коэффициент, который является просто большим целым числом, а также отрицательный показатель. На Intel архитектуре я могу распаковать это практически бесплатно. С точки зрения научных вычислений этот формат также делает все, что нужно. Он соответствует и нуждам бизнеса. Поэтому я предлагаю его как единственный численный тип в будущих прикладных языках (у системных языков другие потребности, и поэтому они будут делать другие вещи).
Я написал реализацию DEC64 на ассемблере Intel x64 — используйте ее, если хотите опробовать тип в аппаратной реализации (dec64.com, GitHub).
Этот тип избавит от необходимости иметь отдельный int. Кроме того, он правильно считает деньги и выполняет арифметические операции так, как нас учили это делать в средней школе.
Мое предложение сделать этот тип единственным означает, что я должен убедить всех в этом мире, что нужно делать именно так. И я догадываюсь, насколько это сложно. Но на самом деле мне не нужно убеждать всех в этом мире. Мне нужно убедить только одного человека — мужчину или женщину, который(которая) разрабатывают следующий язык программирования. Если я смогу убедить этого человека, данный тип, который хорошо работает для людей, станет единственным.
У меня есть немного опыта в сфере убеждения всего мира в том, как поступать правильно. Пример — мой любимый формат обмена данными JSON.
Это статистика Google, которая показывает относительную популярность XML (красным) и JSON (синим). Вы можете видеть, что мир неуклонно теряет интерес к XML с 2005 года, и он очень медленно растет интерес к JSON.
Стоит отметить, что на рост популярности JSON не повлияла никакая отрасль. Нет крупных корпораций, которые продают большие инструменты JSON, потому что вам не нужны большие инструменты для работы с JSON. Вот почему вам он нравится. Это просто работает.
Похоже, скоро будет пересечение. Я не могу предсказать, когда это произойдет. Но Google может. Согласно прогнозам Google Trends это произойдет в этом году (2015 — прим. ред.). JSON наконец станет официально более интересным, чем XML.
Если вам интересна оригинальная видеозапись доклада на английском, с радостью ее предоставляем:
На грядущем HolyJS Piter вы сможете прослушать два доклада Дугласа Крокфорда:
Кроме них в течение двух дней вы услышите еще более 20 докладов о JavaScript: от новых фронтенд фреймворков до разбора серверного перфоманса и десктопной разработки. Будут доклады от Lea Verou, Martin Splitt, Anjana Vakil, Claudia Hernández и много кого еще. Детали смотрите на сайте конференции.