О вреде синтаксического сахара
О чём речь? Конечно, использование синтаксического сахара не приводит к синтаксическому диабету, но он может мешать вам думать. Это может звучать странно, учитывая, что синтаксический сахар призван облегчить нам жизнь: обернуть в интуитивные обёртки операции над абстракциями, сделать программы легко читаемыми, да и просто симпатичными. Однако, всякий инструмент, который направляет нашу мысль одновременно удерживает её на этом направлении.
Ситуация здесь как в анекдоте о дорогах, в котором американец хвастается:
— Я когда еду по дороге, ставлю стакан с водой на капот и гоню со скоростью 100 км/час. Ни одна капля воды не расплескается.
На что русский говорит:
— А мы все залезаем на заднее сиденье и всю дорогу в карты режемся, да пиво пьём.
— А кто же машиной управляет.
— Да куда она, на хрен, из колеи денется.
Смешно, но, однако, удобно, я бы даже позавидовал тем ребятам на заднем сиденье если бы у меня не было синтаксического сахара. Колея хороша тем, что она позволяет не думать о том, как ты едешь, синтаксический сахар позволяет не думать о том, как ты пишешь, освобождая мозг для концентрации на задаче. Но что если для решения задачи мне понадобится сделать что-то странное? Съехать с колеи? Проблема даже не в том, что из неё сложно выбраться, она в том, что это колея для наших мыслей — нам просто не придёт в голову, что есть другие способы сделать дело.
Но довольно теории, пришло время замарать руки. Приведу пример. Допустим, мы пишем ORM и в определённый момент нам понадобилось получить список имен полей модели, чтобы, к примеру, составить SQL запрос. Также, допустим, что поля — это объекты, у которых есть свойство name, содержащее имя поля. Всего-то делов! Надо просто пробежаться по всем полям, вытащить имя и составить список:
var names = [];
for ( var i = 0 , l = fields.length; i < l; i ++ ) names.push(fields[i].name);
>
Похоже, мы пишем ORM на javascript-е, ну ничего, люди делают куда более странные вещи в наши дни. И кстати, мы попали в первую ловушку синтаксиса — «пробежаться» интерпретировали как цикл, и в результате получили пару переменных, сравнение целых чисел, инкремент и квадратные скобки. Чёрт возьми! Это не то, что я имел в виду, когда говорил «пробежаться»! Попробуем ещё раз:
var names = fields.map( function (field) return field.name;
>);
Короче, симпатичнее и куда ближе к тому, как я описывал алгоритм: пробежаться по полям — fields.map, получить имя — функция, которая получает имя. Н-да, функция, как бы от неё избавиться? Легко, не будем сдерживать себя, перепишем всё на питоне:
names = [field . name for field in fields]
Одна строчка! Казалось бы, мы достигли идеала, но на самом деле этот вариант в некотором смысле даже хуже предыдущего: его тяжело расширять и практически невозможно повторно использовать. Да ну? Давайте попробуем, предположим нам вдруг понадобилось иногда приписывать алияс таблицы перед именами полей:
names = [ » %s . %s » % (alias, field . name) if alias
else field . name for field in fields]
Это уже не выглядит так здорово, а что если нам потребуется приписать алияс для поля? И ведь потребуется, не это так что-то ещё. Что ж не будем ждать, когда наш код выйдет из под контроля, отрефакторим его превентивно:
if alias:
names = [ » %s . %s » % (alias, field . name) for field in fields]
else :
names = [field . name for field in fields]
Ясность вернулась, но появилось дублирование. Это не случайно, списковые выражения, которые мы используем связывают в одну синтаксическую структуру пробежку и операцию над отдельным элементом. Не проблема, просто должно остаться только одно списковое выражение:
if alias:
get_name = lambda field: » %s . %s » % (alias, field . name)
else :
get_name = lambda field: field . name
names = map (get_name, fields)
… или ни одного — как только мы избавились от запутывания пробежки и операции списковое выражение стало ненужным. Тут есть ещё один интересный момент — мы вернулись к тому, что у нас было в javascript-е. Т. е. отсутствие в языке такого сладкого элемента как списковые выражения, привело к написанию более универсального кода. Неправда ли отличный пример «less is more», товарищи?
Итак, я «расправился» с циклом for и питоньими списковыми выражениями, пора идти дальше. Вам нравится обращаться к свойствам объекта через точку? Мне — очень, это кратко (кроме самого объекта и требуемого свойства присутствует только маленькая точка) и экспрессивно (даже человек далёкий от объектов легко поймёт в чём тут соль). Это настолько удобно, что мы не вспоминаем об альтернативах, а в том же питоне есть три способа получить атрибут объекта (прямое обращение к __getattr__ и т.п. — чит, не дающий ничего принципиально нового):
obj . name
getattr (obj, «name» )
operator . attrgetter( «name» )(obj)
Нас интересует последний, самый жуткий вариант, потому что он превращает операцию доступа к атрибуту в функцию. Ту самую, которую мы эмулируем с помощью лямбды. Если бы это был единственный способ получить атрибут, то мы сразу бы написали универсальный, расширяемый и готовый к повторному использованию код:
from operator import attrgetter
names = map (attrgetter( «name» ), fields)
Может сложиться впечатление, что я предлагаю отказаться от синтаксиса — нет, это важная часть большинства современных языков, обеспечивающая читаемость и экспрессивность кода. В конце концов, я тоже не против в меру подсластить код. Что я хочу сказать — важно видеть за синтаксисом суть того, что ты делаешь, уметь отодвинуть синтаксис так, чтобы код выражал задачу и чтобы отдельные части задачи ложились на отдельные синтаксические элементы.
P.S. Я не пишу ORM на javascript-е.
P.P.S. Я не пишу ORM и на питоне, хотя временами я копаюсь в ORM Django.
P.P.P.S. Странная идея, изложенная здесь, пришла ко мне во время чтения Practical Common Lisp. Для тех, кто не в курсе, программа на лиспе представляет собой набор вложенных списков, каждый из которых состоит из “что делать” (имени функции, оператора или макроса) и последующих аргументов, т.е. представляет синтаксическое дерево самой себя. Другими словами в Lisp нет синтаксиса. И как ни странно, это делает программы на нём удивительно гибкими.
UPDATE. Чтобы ответить на большинство возражений, подойду несколько с другой стороны. Заметим, что map(), который я в конце концов использую, тоже абстракция довольно высокого уровня. На самом деле используемые мной абстракции можно выстроить в иерархию:
C-style for + абстрагирование от индексирования = for-in
for-in + возврат результата на каждой итерации = map
map + lambda = списковое выражение.
Я начинаю с низкого уровня и дохожу до уровня абстракции, который лучшим образом выражает то, что я пытаюсь сделать. И если мне не нужно обобщать, то здесь я и должен остановиться, но если обобщать приходиться я должен вспомнить, что списковое выражение — это просто map и lambda в одном флаконе или начать дублировать код. Если в языке нет списковых выражений (как в js), то я сразу получу обобщённый код, но он будет более низкоуровневым. Если я забуду о том, что списковое выражение можно разбить, то начну дублировать код.
Подытоживая:
1. Отсутствие определённого синтаксиса в языке приводит к написанию более гибкого кода.
2. Этот более гибкий код будет более низкоуровневым.
Второе плата за первое,
Синтаксический сахар
- Синтаксический сахар (англ. syntactic sugar) в языке программирования — это синтаксические возможности, применение которых не влияет на поведение программы, но делает использование языка более удобным для человека.
Связанные понятия
Идиома программирования — устойчивый способ выражения некоторой составной конструкции в одном или нескольких языках программирования. Идиома является шаблоном решения задачи, записи алгоритма или структуры данных путём комбинирования встроенных элементов языка.
Каламбур типизации является прямым нарушением типобезопасности. Традиционно возможность построить каламбур типизации связывается со слабой типизацией, но и некоторые сильно типизированные языки или их реализации предоставляют такие возможности (как правило, используя в связанных с ними идентификаторах слова unsafe или unchecked). Сторонники типобезопасности утверждают, что «необходимость» каламбуров типизации является мифом.
По одной из классификаций, языки программирования неформально делятся на сильно и слабо типизированные (англ. strongly and weakly typed), то есть обладающие сильной или слабой системой типов. Эти термины не являются однозначно трактуемыми, и чаще всего используются для указания на достоинства и недостатки конкретного языка. Существуют более конкретные понятия, которые и приводят к называнию тех или иных систем типов «сильными» или «слабыми».
Побо́чные эффе́кты (англ. side effects) — любые действия работающей программы, изменяющие среду выполнения (англ. execution environment). Например, к побочным эффектам относятся.
Область видимости (англ. scope) в программировании — часть программы, в пределах которой идентификатор, объявленный как имя некоторой программной сущности (обычно — переменной, типа данных или функции), остаётся связанным с этой сущностью, то есть позволяет посредством себя обратиться к ней. Говорят, что идентификатор объекта «виден» в определённом месте программы, если в данном месте по нему можно обратиться к данному объекту. За пределами области видимости тот же самый идентификатор может быть.
Объектами первого класса (англ. first-class object, first-class entity, first-class citizen) в контексте конкретного языка программирования называются элементы, которые могут быть переданы как параметр, возвращены из функции, присвоены переменной.
Функции первого класса являются неотъемлемой частью функционального программирования, в котором использование функций высшего порядка является стандартной практикой. Простым примером функции высшего порядка будет функция Map, которая принимает в качестве своих аргументов функцию и список и возвращается список, после применения функции к каждому элементу списка. Чтобы язык программирования поддерживал Map, он должен поддерживать передачу функций как аргумента.
Система типов — совокупность правил в языках программирования, назначающих свойства, именуемые типами, различным конструкциям, составляющим программу — таким как переменные, выражения, функции или модули. Основная роль системы типов заключается в уменьшении числа багов в программах посредством определения интерфейсов между различными частями программы и последующей проверки согласованности взаимодействия этих частей. Эта проверка может происходить статически (на стадии компиляции) или динамически.
Ленивые вычисления (англ. lazy evaluation, также отложенные вычисления) — применяемая в некоторых языках программирования стратегия вычисления, согласно которой вычисления следует откладывать до тех пор, пока не понадобится их результат. Ленивые вычисления относятся к нестрогим вычислениям. Усовершенствованная модель ленивых вычислений — оптимистичные вычисления — переходит в разряд недетерминированных стратегий вычисления.
Блок (также говорят блок кода, блок команд, блок инструкций) в программировании — это логически сгруппированный набор идущих подряд инструкций в исходном коде программы, является основой парадигмы структурного программирования.
Вывод типов (англ. type inference) — в программировании возможность компилятора самому логически вывести тип значения у выражения. Впервые механизм вывода типов был представлен в языке ML, где компилятор всегда выводит наиболее общий полиморфный тип для всякого выражения. Это не только сокращает размер исходного кода и повышает его лаконичность, но и нередко повышает повторное использование кода.
Пара́метр в программировании — принятый функцией аргумент. Термин «аргумент» подразумевает, что конкретно и какой конкретной функции было передано, а параметр — в каком качестве функция применила это принятое. То есть вызывающий код передает аргумент в параметр, который определен в члене спецификации функции.
Перегрузка операторов в программировании — один из способов реализации полиморфизма, заключающийся в возможности одновременного существования в одной области видимости нескольких различных вариантов применения оператора, имеющих одно и то же имя, но различающихся типами параметров, к которым они применяются.
Зарезерви́рованное сло́во (или ключево́е сло́во) — в языках программирования слово, имеющее специальное значение. Идентификаторы с такими именами запрещены.
В языках программирования объявле́ние (англ. declaration) включает в себя указание идентификатора, типа, а также других аспектов элементов языка, например, переменных и функций. Объявление используется, чтобы уведомить компилятор о существовании элемента; это весьма важно для многих языков (например, таких как Си), требующих объявления переменных перед их использованием.
Примитивный (встроенный, базовый) тип — тип данных, предоставляемый языком программирования как базовая встроенная единица языка.
Присва́ивание — механизм связывания в программировании, позволяющий динамически изменять связи имён объектов данных (как правило, переменных) с их значениями. Строго говоря, изменение значений является побочным эффектом операции присваивания, и во многих современных языках программирования сама операция также возвращает некоторый результат (как правило, копию присвоенного значения). На физическом уровне результат операции присвоения состоит в проведении записи и перезаписи ячеек памяти или регистров.
Чистота́ (в отношении языка программирования) — отсутствие побочных эффектов. Язык программирования является чистым в том случае, если все функции в программах этого языка являются чистыми.
В информатике типобезопасность (англ. type safety) языка программирования означает безопасность (или надёжность) его системы типов.
Анонимная функция в программировании — особый вид функций, которые объявляются в месте использования и не получают уникального идентификатора для доступа к ним. Поддерживаются во многих языках программирования.
Из-за путаницы с терминологией словом «оператор» в программировании нередко обозначают операцию (англ. operator), см. Операция (программирование).Инстру́кция или опера́тор (англ. statement) — наименьшая автономная часть языка программирования; команда или набор команд. Программа обычно представляет собой последовательность инструкций.
Перегрузка процедур и функций — возможность использования одноимённых подпрограмм: процедур или функций в языках программирования.
Конста́нта в программировании — способ адресации данных, изменение которых рассматриваемой программой не предполагается или запрещается.
Фу́нкция вы́сшего поря́дка — в программировании функция, принимающая в качестве аргументов другие функции или возвращающая другую функцию в качестве результата. Основная идея состоит в том, что функции имеют тот же статус, что и другие объекты данных. Использование функций высшего порядка приводит к абстрактным и компактным программам, принимая во внимание сложность производимых ими вычислений.
В программировании сборка мусора (англ. garbage collection) — одна из форм автоматического управления памятью. Специальный процесс, называемый сборщиком мусора (англ. garbage collector), периодически освобождает память, удаляя объекты, которые уже не будут востребованы приложениями.
Динами́ческая типиза́ция — приём, широко используемый в языках программирования и языках спецификации, при котором переменная связывается с типом в момент присваивания значения, а не в момент объявления переменной. Таким образом, в различных участках программы одна и та же переменная может принимать значения разных типов. Примеры языков с динамической типизацией — Smalltalk, Python, Objective-C, Ruby, PHP, Perl, JavaScript, Lisp, xBase, Erlang, Visual Basic.
Сравне́ние в программировании — общее название ряда операций над па́рами значений одного типа, реализующих математические отношения равенства и порядка. В языках высокого уровня такие операции, чаще всего, возвращают булево значение («истина» или «ложь»).
Опера́ция — конструкция в языках программирования, аналогичная по записи математическим операциям, то есть специальный способ записи некоторых действий.
В информатике лексический анализ («токенизация», от англ. tokenizing) — процесс аналитического разбора входной последовательности символов на распознанные группы — лексемы, с целью получения на выходе идентифицированных последовательностей, называемых «токенами» (подобно группировке букв в словах). В простых случаях понятия «лексема» и «токен» идентичны, но более сложные токенизаторы дополнительно классифицируют лексемы по различным типам («идентификатор, оператор», «часть речи» и т. п.). Лексический.
Запись — агрегатный тип данных, инкапсулирующий без сокрытия набор значений различных типов.
Ссылочная прозрачность и ссылочная непрозрачность — это свойства частей компьютерных программ. Выражение называется ссылочно прозрачным, если его можно заменить соответствующим значением без изменения поведения программы. В результате вычисления ссылочно прозрачной функции дает одно и то же значение для одних и тех же аргументов. Такие функции называются чистыми функциями.
Переменная типа (ти́повая переменная) в языках программирования и теории типов — переменная, которая может принимать значение из множества типов данных.
Неопределённое поведение (англ. undefined behaviour, в ряде источников непредсказуемое поведение) — свойство некоторых языков программирования (наиболее заметно в Си), программных библиотек и аппаратного обеспечения в определённых маргинальных ситуациях выдавать результат, зависящий от реализации компилятора (библиотеки, микросхемы) и случайных факторов наподобие состояния памяти или сработавшего прерывания. Другими словами, спецификация не определяет поведение языка (библиотеки, микросхемы) в любых.
Алгебраи́ческий тип да́нных — в информатике наиболее общий составной тип, представляющий собой тип-сумму из типов-произведений. Алгебраический тип имеет набор конструкторов, каждый из которых принимает на вход значения определённых типов и возвращает значение конструируемого типа. Конструктор представляет собой функцию, которая строит значение своего типа на основе входных значений. Для последующего извлечения этих значений из алгебраического типа используется сопоставление с образцом.
Ме́тод в объектно-ориентированном программировании — это функция или процедура, принадлежащая какому-то классу или объекту.
Раскрутка компилятора (англ. bootstrapping — от boot и strap) — метод создания транслятора для некоторого языка программирования, при котором транслятор пишется на том же языке программирования, для трансляции которого создаётся; создание транслятором исполняемых файлов из исходного кода самого транслятора. Используется для переноса трансляторов на новые платформы. Появился в середине 1950-х годов. Позволяет создать транслятор, который генерирует сам себя. Применялся для создания трансляторов многих.
Литерал (англ. literal ) — запись в исходном коде компьютерной программы, представляющая собой фиксированное значение. Литералами также называют представление значения некоторого типа данных.
Мона́да — это абстракция линейной цепочки связанных вычислений. Монады позволяют организовывать последовательные вычисления.
Динамическая идентификация типа данных (англ. run-time type information, run-time type identification, RTTI) — механизм в некоторых языках программирования, который позволяет определить тип данных переменной или объекта во время выполнения программы.
Программи́рование ме́тодом копи́рования-вста́вки, C&P-программирование или копипаста в программировании — процесс создания программного кода с часто повторяющимися частями, произведёнными операциями копировать-вставить (англ. copy-paste). Обычно этот термин используется в уничижительном понимании для обозначения недостаточных навыков компьютерного программирования или отсутствия выразительной среды разработки, в которой, как правило, можно использовать подключаемые библиотеки.
Переме́нная в императивном программировании — поименованная, либо адресуемая иным способом область памяти, адрес которой можно использовать для осуществления доступа к данным. Данные, находящиеся в переменной (то есть по данному адресу памяти), называются значением этой переменной.
Синтаксис языка программирования — набор правил, описывающий комбинации символов алфавита, считающиеся правильно структурированной программой (документом) или её фрагментом. Синтаксису языка противопоставляется его семантика. Синтаксис языка описывает «чистый» язык, в то же время семантика приписывает значения (действия) различным синтаксическим конструкциям.
Получение ресурса есть инициализация (англ. Resource Acquisition Is Initialization (RAII)) — программная идиома объектно-ориентированного программирования, смысл которой заключается в том, что с помощью тех или иных программных механизмов получение некоторого ресурса неразрывно совмещается с инициализацией, а освобождение — с уничтожением объекта.
Абстра́ктный тип да́нных (АТД) — это математическая модель для типов данных, где тип данных определяется поведением (семантикой) с точки зрения пользователя данных, а именно в терминах возможных значений, возможных операций над данными этого типа и поведения этих операций.
Замыкание (англ. closure) в программировании — функция первого класса, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся её параметрами. Говоря другим языком, замыкание — функция, которая ссылается на свободные переменные в своей области видимости.
В программировании термин «директива» (указание) по использованию похож на термин «команда», так как также используется для описания некоторых конструкций языка программирования (то есть указаний компилятору или ассемблеру особенностей обработки при компиляции).
Таблица виртуальных методов (англ. virtual method table, VMT) — координирующая таблица или vtable — механизм, используемый в языках программирования для поддержки динамического соответствия (или метода позднего связывания).
Императи́вное программи́рование — это парадигма программирования (стиль написания исходного кода компьютерной программы), для которой характерно следующее.
Межпроцедурная оптимизация (англ. Interprocedural Optimization, IPO), или полнопрограммная оптимизация программ (англ. whole program optimization) — оптимизация компилятора, которая использует глобальный анализ потока управления и затрагивает множество процедур, даже находящихся в разных модулях, за счёт чего может достигаться существенный прирост быстродействия.
Почему async/await больше, чем синтаксический сахар
Несмотря на то, что тысячи статей об async/await и Promise уже существуют, многие из них оставляют желать лучшего. Поэтому хочу написать свою статью на эту тему.
В этой статье я хотел бы подчеркнуть, что async/await — это больше, чем синтаксический сахар поверх Promise , поскольку async/await предлагает ощутимые преимущества:
- async/await позволяет использовать все языковые конструкции, доступные в синхронном программировании, что приводит к более выразительному и читабельному коду;
- async/await объединяет возможности асинхронного программирования;
- async/await обеспечивает лучшую трассировку стека ошибок;
Эта статья предполагает наличие базовых знаний о Promise и async/await . Она не для того, чтобы конкурировать с руководствами на MDN и javascript.info (learn.javascript.ru).
Немного истории асинхронного программирования в JavaScript
Асинхронное программирование распространено в JavaScript. Всякий раз, когда нам нужно сделать вызов веб-сервиса, получить доступ к файлу или выполнить операцию с базой данных. Асинхронность — это то, как мы предотвращаем блокировку пользовательского интерфейса, несмотря на то, что язык является однопоточным.
До серьёзного обновления JavaScript в ES2015 (ES6) обратные вызовы были тем, чем люди справлялись с асинхронным программированием. Единственный способ выразить временную зависимость (то есть порядок выполнения асинхронных операций) — это вложить один обратный вызов в другой. Это привило к так называемому Callback Hell / Ад обратных вызовов .
Пользователь Reddit @theQuandary отметил, что до ES6 существовали другие лучшие альтернативы асинхронному программированию JavaScript, чем обратные вызовы. Извините, что не на 100% точен, так как большая часть этой истории прошла мимо меня.
Затем Promise был представлен в JavaScript в ES2015. Это первоклассный объект для асинхронных операций, который можно легко передавать, компоновать, агрегировать и применять к нему преобразования. Временная зависимость чётко выражается через цепочку методов then .
Ещё немного истории…
Идея Promise в JavaScript не была оригинальной. Она была вдохновлена старым языком под название E. Его создатель Марк Миллер также является представителем TC39. А синтаксис `async/await` был заимствован из C#.
Поскольку Promise служит мощным примитивом, похоже, что асинхронное программирование — это решённая проблема в JavaScript, верно?
Ну, пока не совсем, потому что иногда уровень Promise может быть слишком низким для работы с…
Иногда Promise может быть слишком низкого уровня для работы с
Несмотря на появление Promise , по-прежнему существовала потребность в языковой конструкции более высокого уровня для асинхронного программирования в JavaScript.
Давайте рассмотрим пример, где нужна функция для опроса API с некоторым интервалом. Она разрешает значение null , когда достигнуто максимальное количество повторных попыток.
Вот одно из возможных решений с Promise :
let count = 0;
function apiCall()
return new Promise((resolve) =>
// на шестой попытке, разрешает `value`
count++ === 5 ? resolve('value') : resolve(null)
);
>
function sleep(interval)
return new Promise((resolve) => setTimeout(resolve, interval));
>
function poll(retry, interval)
return new Promise((resolve) =>
// пропускаем обработку ошибок для краткости.
if (retry === 0) resolve(null);
apiCall().then((val) =>
if (val !== null) resolve(val);
else
sleep(interval).then(() =>
resolve(poll(retry - 1, interval));
>);
>
>);
>);
>
poll(6, 1000).then(console.log); // 'value'
Насколько интуитивно понятным и удобочитаемым будет это решение, зависит от знакомства с Promise , от того, как Promise.resolve плоские Promise и рекурсию.
Вместо этого вы можете использовать setInterval
Почти всегда есть другой способ написать функцию. Вот решение с setInterval написанное моим другом Джеймсом:
const pollInterval = (retry, interval) =>
return new Promise((resolve) =>
let intervalToken, timeoutToken;
intervalToken = setInterval(async () =>
const result = await apiCall();
if (result !== null)
clearInterval(intervalToken);
clearTimeout(timeoutToken);
resolve(result);
>
>, interval);
timeoutToken = setTimeout(() =>
clearInterval(intervalToken);
resolve(null);
>, retry * interval);
>);
>;
Вводим async/await
Давайте перепишем приведённое выше решение, используя синтаксис async/await :
async function poll(retry, interval)
while (retry >= 0)
const value = await apiCall().catch((e) => >); // пропускаем обработку ошибок для краткости.
if (value !== null) return value;
await sleep(interval);
retry--;
>
return null;
>
Я ожидаю, что большинство людей сочтут это решение более читабельным, потому что мы можем использовать все нормальные языковые конструкции, такие как циклы, try-catch для асинхронных операций.
Однако это не совсем сравнение яблок с яблоками, поскольку я перешёл от рекурсивного подхода к итеративному подходу. Давайте перепишем приведённое выше решение, используя рекурсию:
const pollAsyncAwait = async (retry, interval) =>
if (retry 0) return null;
const value = await apiCall().catch((e) => >); // пропускаем обработку ошибок для краткости.
if (value !== null) return value;
await sleep(interval);
return pollAsyncAwait(retry - 1, interval);
>;
Это, вероятно, самое большое преимущество async/await — возможность писать асинхронный код синхронным способом. С другой стороны, именно отсюда, вероятно, исходит наиболее распространённое возражение против async/await . Подробнее об этом позже.
Между прочим, await даже имеет правильный приоритет оператора, так что wait a + await b действительно означает (await a) + (await b) , а не, скажем, await(a + await b) .
async/await предлагает унифицированный опыт синхронизации асинхронного кода
Ещё одна приятная особенность async/await заключается в том, что await автоматически превращает любые не-Promise ( non-thenables ) в Promise . Семантика await приравнивается к Promise.resolve , что означает, что вы можете ждать что угодно:
function fetchValue()
return 1;
>
async function fn()
const val = await fetchValue();
console.log(val); // 1
>
// это равно следующему
function fn()
Promise.resolve(fetchValue()).then((val) =>
console.log(val); // 1
>);
>
Обратите внимание, что это поведение зависит от браузера…
Утверждение, что await foo равно Promise.resolve(foo).then(. ) не является на 100% точным.
До Chrome 97 спецификация ECMAScript переводила await foo в Promise.resolve(foo).then(. ) . Затем в этом PR было внесено изменение в спецификацию. Но до сих пор не каждый браузер поддерживал изменения спецификации; на момент написания этой статьи Safari ещё не реализовал обновлённую спецификацию. В результате запуска [этого фрагмента](https://gist.github.com/zhenghaohe/c90ec960b890eca60b7bd8008f856a70) в Safari будет результат отличный от результата Chrome.
Если бы мы присоединили метод then к числу 1 , возвращаемому из fetchValue , возникла бы следующая ошибка:
function fetchValue()
return 1;
>
function fn()
fetchValue().then((val) =>
console.log(val);
>);
>
fn(); // ❌ Uncaught TypeError: fetchValue(. ).then is not a function
Наконец, всё, что возвращается из async функции, всегда Promise :
Object.prototype.toString.call((async function () >)()); // '[object Promise]'
async/await обеспечивает лучшую трассировку стека ошибок
Инженер V8 Матиас написал статью Asynchronous stack traces: why await beats Promise#then() , в которой рассказывается, почему движку легче захватывать и хранить трассировку стека для async/await по сравнению с Promise .
async function foo()
await bar();
return 'value';
>
function bar()
throw new Error('BEEP BEEP');
>
foo().catch((error) => console.log(error.stack));
// Error: BEEP BEEP
// at bar (:7:9)
// at foo (:2:9)
// at :10:1
Асинхронная версия корректно захватывает трассировку стека ошибок.
Давайте посмотрим на Promise версию:
function foo()
return bar().then(() => 'value');
>
function bar()
return Promise.resolve().then(() =>
throw new Error('BEEP BEEP');
>);
>
foo().catch((error) => console.log(error.stack));
// Error: BEEP BEEP at :7:11
Трассировка стека потеряна. Переключение с анонимной стрелочной функции на объявление именованной функции немного помогает, но ненамного:
function foo()
return bar().then(() => 'value');
>
function bar()
return Promise.resolve().then(function thisWillThrow()
throw new Error('BEEP BEEP');
>);
>
foo().catch((error) => console.log(error.stack));
// Error: BEEP BEEP
// at thisWillThrow (:7:11)
Распространённые возражения против async/await
Я видел два распространённых возражения против async/await .
Первое, async/await может выстрелить в ногу, когда кто-то излишне секвенирует независимые вызовы асинхронных функций, когда они могут обрабатываться параллельно (или параллельно , если мы используем этот термин в широком смысле) с Promise.all .
Обычно это происходит, когда люди пытаются разобраться с асинхронным программированием, не понимая, как работает Promise за кулисами.
Во втором больше нюансов. Некоторые энтузиасты функционального программирования считают, что async/await предполагает программирование в императивном стиле. С точки зрения FP-программиста, возможность использовать циклы и try catch не является благом, поскольку эти языковые конструкции подразумевают побочные эффекты и способствуют неидеальной обработке ошибок.
Я симпатизирую этому аргументу. FP-программисты по праву заботятся об определённости в своих программах. Они хотят быть абсолютно уверенными в своём коде. Чтобы добиться этого необходима сложная система типов с такими типами, как Result . Но я не думаю, что async/await сам по себе несовместим с FP. Мой друг Джеймс, специалист по FP, сказал, что в Haskell есть аналог async/await — возможности Do-нотации.
В любом случае, я думаю, что для большинства людей, включая меня, FP остаётся приобретённым вкусом (хотя я действительно думаю, что FP это очень круто, и я медленно изучаю его). Обычные операторы управления потока и обработки ошибок try catch , предоставляемые async/await , бесценны для нас при организации сложных асинхронных операций в JavaScript. Именно поэтому, сказать, что async/await — это просто синтаксический сахар , — это преуменьшение.
База по языкам программирования: Синтаксический сахар или история развития языков
Продолжаю выкладывать выдержки из вводного курса нашей компании по промышленному программированию.
Часть третья: Синтаксический сахар или история развития языков
В данной части расказывается история развития языков программирования, а так же доступно объясняется что такое ООП и функциональное программирование. Другие части можно найти тут.
Синтаксический сахар (syntactic sugar) — общее обозначение дополнений к синтаксису ЯП, которые делают использование языка более удобным, но не добавляют ему новых возможностей.
Вся история развития ЯП — это история повышения сладости синтаксического сахара.
Машинные языки
Всё началось с машинно-зависимых языков — языков, учитывающий структуру и характеристики определённых компьютерных платформ. Те, кто программировал на калькуляторах помнят, как составлялись на них программы.
Десяток регистров, куда записывались результаты вычислений (где они, эти гигабайты оперативки?), пара регистров смещения (вспомните машину Тьюринга, да-да, регистры обозначали из какого регистра данных брать следующую команду!), и регистр команды, куда нужно было записать очередную операцию (прочитать значение, записать значение, сложить значение двух регистров памяти и т.д.).
Архитектура фон Неймана
Даже архитектура этих устройств не всегда соответствовала архитектуре фон Неймана — стандартной для современных компьютеров.
Собственно, архитектура фон Неймана подразумевает отделение памяти от процессора и хранение в памяти изменяемых программ. Калькуляторы же обычно являлись устройствами с фиксированным набором выполняемых программ.
Собственно, переход к архитектуре фон Неймана породил возможность задавать автоматически выполняющиеся программы из внешнего источника — поначалу из перфолент и перфокарт.
Люди так и программировали, пробивая отверстие в карточке, соответствующее определённому регистру, таким образом, побитово задавая значения в них. Много историй связано с тем, как программа, набитая на сотнях перфокарт в буквальном смысле этого слова рассыпалась, когда неуклюжие техники роняли эти стопки картона на пол.
Ассемблер
На машинных кодах программировать было не очень удобно, по этому при первой же возможности появился ассемблер — язык, повторяющий машинные операции, но с человекопонятными командами и возможностью ручкой по бумаге описать алгоритм не как набор битов, а как какой-то более осмысленный текст.
Ассемблер так же привязан к архитектуре машины (поскольку его команды повторяют команды процессора), но шаг в пропасть был уже сделан и языки начинали всё больше и больше обрастать кристаллами сахара.
Стековые языки
Первой ласточкой стало использование стеков данных. Стек появился для решения задачи временного хранения произвольных данных. Конечно, данные можно сохранять и в регистре, однако в этом случае нужно помнить имя каждого регистра, данные из которого хочется получить.
Характерностью стека является особый порядок получения из него данных: в любой момент времени в стеке доступен только верхний элемент, т.е. элемент, загруженный в стек последним. Выгрузка из стека верхнего элемента делает доступным следующий элемент, по аналогии с автоматным рожком — первый засунутый туда патрон достать можно только последним.
Сейчас это может казаться дико неудобным, но это позволило создавать подпрограммы.
Перед вызовом подпрограммы, мы заполняем специально именованный стек данными. Подпрограмма, зная, в каком порядке помещены в стек параметры, может забрать их оттуда и использовать при своем выполнении, а по выполнении поместить результаты своего труда в тот же или в другой стек. Кроме того, основная программа имеет возможность сохранить свои данные в стеке до передачи управления подпрограмме. После возврата контроля программа просто восстанавливает свои значения из стека и не обращает внимание на то, что данные в регистрах процессора могли подпрограммой перетираться.
Макроассемблер
Следующим шагом был макроассемблер. Макроассемблер — это программа для макропроцессора, который в свою очередь являлся транслятором с языка более высокого уровня (макроассемблера) в машинный код. Стало возможным создавать свои команды для, например, использования стека.
Рождаются команды работы со стеком (push, pop), команды копирования стеков данных.
Макроассемблер порождает языки более высокого уровня, за командами которых стоят десятки, а то и сотни команд процессора. FORT, ALGOL, BASIC начинают свой путь…
Модульные языки
Вкусив запретного плода расширенного синтаксиса, программисты не остановились и возжелали модульности: ведь это так удобно — вызывать отдельно написанный модуль программы и не вникать в его алгоритм. Главное — это знать как он принимает на вход данные и как возвращает результат.
Ассемблер пополняется командами, облегчающими именование и подключение модулей, передачу и возврат управления при вызове различных подпрограмм. Развиваются интерфейсы обмена данными. Возникает понятие структуры данных.
Процедурные языки
Логичным добавлением к модульнуму языку послужило понятие процедуры или подпрограммы. Подпрограмма обладает двумя важными особенностями:
1. она именованна, т.е. мы можем вызвать подпрограмму по имени
2. вызвав подпрограмму, мы точно знаем, что она вернёт управление в то же место, откуда была вызвана
К примеру, в BASIC подпрограмма вызывалась как GOSUB :Label:.
Функция
Не хватало только одного: хотелось, чтобы переменные материнской программы (из которой вызывалась подпрограмма) не портились. А то ведь как оно было? Все переменные в глобальном пространстве, начнёшь их же использовать в подпрограмме — она их и затирает.
Так было изобретено понятие функции и локальных переменных: мы вызываем именованную подпрограмму и передаём туда какие-то значения. Подпрограмма воспринимает переданные значения, как локальные именованные переменные.
С развитием функция обросла возможностью возвращать результат: до этого, ведь, как было — возвращаемое значение записывали в одну из глобальных переменных.
Функция обладает следующими особенностями:
1. она именованна
2. туда передаются параметры
3. переданные параметры доступны как именованные параметры только внутри функции, вне функции они не видны
4. функция может использовать свои локальные именованные параметры, не видные вне этой функции
5. функция может возвращать результат работы
Введение в синтаксис функции гармонично дополняет процедурные языки программирования.
Функциональные языки
Естественным желанием было дополнить функции возможностью наблюдать при вызове локальные переменные функции-родителя в вызываемой функции.
Для решения этого гении сумеречного разума порождают понятие контекста исполнения: это область именованных переменных, доступных функции во время выполнения. Эта область данных делается наследовательно-расширяемой: при при вызове дочерней функции она создаёт свой контекст, пополняемый переменными, объявляемыми внутри функции-дочки. При этом вне функции-дочки эти переменные не видны. Зато будут доступны при вызове функции-внучки, функции-правнучки и так далее.
Возможность наследовать контекст исполнения называют замыканием (closure).
Возможность полноценной работы с контекстом исполнения порождает функциональные языки программирования.
Окончательно их оформляет добавление возможности передавать функцию как параметр для вызова другой функции, а так же возвращать функцию в качестве результата выполнения подпрограммы.
Типы данных
В то же самое время мысль программистов не стояла на месте. Программисты изобретали типы данных.
Первоначально, ведь как было, данные были доступны исключительно в бинарном виде — нолики, да единички.
Людям же, для решения практических задач, удобнее оперировать абстракциями более высоких уровней. Так появляются целочисленные типы данных без возможности указать являются ли они отрицательным или нет (byte, unsigned integer, unsigned long integer и т.д.).
Потом, как развитие их — типы данных с возможностью записи отрицательного числа (кодировавшегося первым битом, в связи с чем возникали забавные казусы неравенства +0 и -0). В довершении для более удобной работы с плавающей точкой возникли типы float и double float (как нетрудно догадаться, double float — это тот же float, но с возможностью записать больше знаков как до, так и после запятой).
Интересно байтовое представление типа float — в принципе, для того, чтобы передать число нам необходим тот же integer с возможностью указать отрицательное число и или нет и указанием, через сколько разрядов от начала числа необходимо поставить точку.
Для логических операций, в принципе, вполне хватало того же нуля и единички, но для большей кузявости их обернули в тип boolean с двумя значениями true и false (за которыми, в прочем, стояли те же единичка и нолик).
Следующим типом остро потребовавшийся программистам стал массив. Массив данных кардинально отличался от стека возможностью свободного доступа не только к последнему засунутому элементу, а вообще к любому по номеру. Массив представлялся программистам как склеенные ячейки, внутри которых лежат данные, по этому изначально массив задавался сразу определённого размера и размер этот изменить было невозможно.
Но, ведь, ячейки не обязательно бывают заполненными? Так потребовалось обозначение пустой ячейки и возникает тип null. По факту, вначале он представлял из себя символ с кодом 0x0, что приводило к весёлым казусам, когда в эту самую ячейку требовалось записать нулевое значение, а потом прочитать его и интерпретировать именно как null, а не как unsigned integer со значением 0.
Для объявления массива резервировался фрагмент памяти (буфер) с указанием, сколько ячеек будет расположено в этом фрагменте, а так же какие элементы будут в нём размещаться. И не дай Ричи тебе записать в массив типа int элемент вида long! В лучшем случае повреждались последующие за ним элементы, в худшем — возникало переполнение буфера и могли повредиться другие, не относящиеся к массиву данные, расположенные сразу же за выделенным буфером памяти.
Строки, кстати, вначале появились именно как массивы символов (пришлось ввести ещё один типа данных — char, по сути соответствовавший byte). Из-за этого длину строк приходилось объявлять заранее.
Дабы справиться со строками переменной длины, придумали помечать null-маркером конец строки. Т.е., как и ранее строка представляла из себя массив, но длина этого массива задавалась сразу достаточно большой, чтобы вместить любую строку (640кб памяти хватит любым программам, ага). Строка начиналась с начала массива, а конец её помечался как null-байт, то что шло после null строкой не считалось.
Хорошая на бумаге идея помечать конец строки null-маркером при ближайшем рассмотрении оказалось ужасной: ничто не мешало добавить в середину строки null и поиметь с этого кучу лулзов. Так началась эра C-strings.
Ссылки
Организация работы с данными, как с буфером памяти породило интересную возможность при вызове функции передавать туда не сами данные, а ссылку на них.
Ранее ведь как было? В функции передавались значения переменных, значения эти копировались в именованные переменные функции дабы избежать порчи оригинальных данных.
Но ведь внутрь функции можно передать просто значение адреса выделенного фрагмента памяти и далее зачитать из него переменную любого доступного типа данных! Так появился ещё один тип данных — ссылка.
Ссылка представляет из себя ярлычок (link) на какую-то переменную, под которую выделен блочок памяти. В ЯП появляются методы работы с переменными как по значению (напрямую с этим блоком памяти), так и по ссылке (считываем из переменной указатель, потом идём по нему и уже там меняем значение в памяти).
Структуры данных
Казалось бы, за что боролись на то и напоролись: изолировали-изолировали переменные внутри функций, чтобы их не портить, а теперь даём в руки ружьё из которого ещё и в ногу можно выстрелить!
Но не тут то было: передача переменных по ссылке дало уникальную возможность конструировать из простых типов данных целые конструкции — структуры данных!
К примеру, стало возможно организовать ссылку на массив из ссылок на массивы из… Так это же целое дерево построить можно!
Естественно, сам по себе такой массив никакой практической ценности не несёт, поскольку всё это можно организовать и с помощью простых типов данных, но если добавить в программу функции типа addNode, removeNode, работающие с деревом и передавать в эти функции ссылку на структуру данных, то получается рабочая и весьма соблазнительная конструкция.
Структурные языки
Так это же получается, что программист сам может создавать свои типа данных, удобных для его программы — достаточно только создать структуру данных и описать функции работы с ними!
Так появляются структурные языки программирования. В них немедленно добавляют возможность описания нового типа данных, возможность как-то именовать этот тип и задавать для него какие-то операции.
К примеру, строку можно представить уже не просто массивом, а двусвязанным списком с функциями конкатенации через оператор + и доступом к произвольному символу через оператор [].
Начинается немедленный бурный рост структурных языков (Pascal, C), обладающих следующими особенностями:
1. в них есть формальный язык описания структур данных (*.h файлы в C)
2. в них есть возможность дать описанной структуре название (BTree)
3. в них есть возможность обозначить операции работы с этой структурой данных
Объект
Возможность создавать свои собственные типы данных возбуждает в программистах страстное желание внутри этого типа данных иметь функции для работы с ним.
Как ответ на чаяния этих светлых умов рождается концепция Объекта. Объект — это уже не просто тип данных, не просто ссылка, по которой хранится структуированная информация, но ещё и функции по обработке этой информации, доступные по этой же ссылке.
Под это подводится вселенская философия:
“Объект — некоторая сущность в виртуальном пространстве, обладающая определённым состоянием и поведением, имеющая заданные значения свойств (атрибутов) и операций над ними (методов).”
Инкапсуляция
Глубокие философские исследования позволяют осознать, что объект обладает таким свойством, как инкапсуляция, которую определяют как свойство объекта объединять в себе данные и методы работы с этими данными. Философы вообще любят рекурсивные определения.
Суть инкапсуляции же проста: объект — не объект, если состояние его (т.е. данные, которые он содержит) можно поменять не применяя методов объекта. При этом считается, что публичные переменные объекта, доступные всем и вся для изменения — это как бы тоже методы по изменению внутреннего состояния этого самого объекта.
Собственно, $object->property = 12345; считается эквивалентным методу $object->setProperty(12345);, ведь без указания в операции имени объекта $object доступа к переменной $property напрямую не получить.
Наследование
Ещё до философов Объекта программисты при работе со структурами данных очень сильно захотели и придумали, как расширять структуры данных, наследовать схему структуры-родителя в структуре-детёныше.
Создание же Объекта, объединяющего данные с функциями породило интересную инженерную задачу, как бы так извернуться: и структуру унаследовать, и функции унаследовать, да ещё бы и новые возможности в наследника добавить.
А всё дело в том, что есть у тебя в Объекте-родителе функция, есть в Объекте-наследнике функция, делают они разное, а вот чтоб имена у них — опа — одинаковые. Решение этой задачи назвали полиморфизмом.
Полиморфизм
Философы и тут подсуетились, дав определение: “Полиморфизм — возможность объектов с одинаковой спецификацией иметь различную реализацию.”. Под спецификацией тут понимается названия-сигнатуры методов работы с объектом (включая публичные переменные).
Реализаций полиморфизма множество, вот некоторые из них:
— чистый полиморфизм (полиморфизм по сигнатуре)
— параметрический полиморфизм (полиморфизм по имени метода)
— переопределение (абстрагирование, абстрактные классы)
— перегрузка (неполное замещение метода предка методом потомка)
Абстрагирование
Философская мысль тоже не стояла на месте. Изучив свойства наследования, философы поняли, что его можно заменить абстрагированием.
Абстрагирование это такая вещь… Как бы объяснить? Вот есть у вас Объект — отлично, это что-то материальное. А ещё есть представление о том, каким этот объект может быть: какие методы он должен выставлять, что эти методы должны делать, но без конкретики, так, абстрактно (напоминает заказчиков, не правда ли?). Собственно мы только что описали интерфейс объекта или абстрактного предка заветы которого можно отлить в реальность кода.
ООП
Собственно, ООП — Объектно Ориентированное Программирование. Это умение некоторых программистов работать с объектами в рамках всех трёх концепций: инкапсуляции, наследования и полиморфизма. Ну или инкапсуляции, абстрагирования и полиморфизма.
Никакого отношения к модели MVC парадигма OOP не имеет (в отличие от мнения некоторых PHP программистов). ООП — это просто работа с данными и методами обработки данных, как с наследуемыми объектами.
В отличие от парадигмы процедурного и структурного программирования, где если и есть объекты, то они не наследуемы. Ну или и объектов-то нет, все данные передаются в массивах, структурах, выделенных буферах памяти.
Класс-ориентированное программирование
Объектное программирование требует создания множества объектов (как ни странно). Соответственно требуются как-то организовать иерархию объектов, как-то скучковать их.
В ответ на эти чаяния была разработана концепция класс-инстанс. Что такое класс? Класс — это набор методов и функций без данных. Сам по себе класс — это нечто нерабочее, для работы нужны данные. Собственно, для того чтобы получить рабочий объект необходимо инстанциировать класс — сказать “создай мне объект с теми функциями, что описанные в этом классе и данными, которые я тебе сейчас скажу”.
Фактически класс — это такой синтаксический кусище сахара, который позволяет не просто описать API объекта (как это делает интерфейс), но и задать функции по обработке данных.
Система классов позволяет формально описать свойства объекта, правила наследования свойств объектов, правила доступа к данным объекта. Использование классов задаёт парадигму класс-ориентированного программирования.
Класс — прикольная штука, но не необходимая для ООП, поскольку бывают объектно-ориентированные языки прекрасно обходящиеся и без классов.
Прототипное программирование
Другим способом задавать наследование является прототип. При прототипном программировании нет инстансов объектов, объект существует в уникальном виде. Но для каждого объекта можно задать прототип или прототипы — список объектов, свойства и методы которых он будет наследовать.
Исторически модель наследования через прототипы, которую разделяют такой язык, как JavaScript, является более старой, чем через описание классов. Но класс-ориентированное программирование оказалось более удобным для описания API и фреймворков (а как известно, каждый половозрелый Java программист обязан написать свой фреймворк, так же как половозрелость PHP программиста определяется по самописной CMS), по этому стало более распространённым.
- образование
- обучение
- теория программирования