JavaScript / Путь асинхронного самурая

в 21:38, , рубрики: async pattern, javascript, monads, асинхронное программирование, монады, метки: , , , ,

JavaScript — очень необычный язык. Может это звучит немного странно, но по-моему в его истории есть некоторое сходство с судьбой японского языка. Он, возможно, не был изначально глубоко продуман и был сделан на скорую руку, но при этом в умелых руках он часто оказывается неожиданно элегантным. Он был “поскрёбан” по различной степени качества сусекам, но при этом он легко впитывает нововведения и иногда даже кажется, что только для них и был создан. Он покорно принимает различные стили письма и, если бы не апологеты, “правильное” написание было бы, возможно, уже забыто… И, самое главное, как и для японского, нет обозримой границы в познании этого языка. Я знаком с ним на протяжении многих лет и он постоянно открывает мне новые грани.

Но одно я знаю точно — на JavaScript можно писать очень простой и понятный код. К этому нужно стремиться и взращивать это в себе, нужно не опускать руки, нужно узнавать новые и новые вещи, уметь принимать их, и рано или поздно обнаруживаешь, что сам язык — довольно-таки прост и при этом очень, просто-таки бесконечно, элегантен.

И впитывает новое, как губка.

Теперь к делу.

Проблема

Большинство жалоб на язык — ООП, которое, как известно, в нём есть, в своём наипрекраснейшем и буквальном виде, в виде прототипов. И даже необходимость в наследовании оказывается довольно-таки надумана, когда понимаешь, как дружить миксины. Но статья, в этот раз, не об этом.

Вторая по категоричности, но первая по сегодняшней моде, жалоба — “неудобство асинхронного программирования”. Она озвучивается тут и там, находят способы один необычнее другого, но все они ненамного проще друг друга настолько, что мне даже не хочется приводить ссылки (хотя приведу пример). Предлагаются сотни похожих библиотек, пишутся монструозные заменители, в общем всё происходит так, как обычно происходит с бедным стареньким, молодым и свежим JavaScript'ом.

…И потом пишутся тонны “образумливающих” статей подобных моей, и только читателю решать, где истина.

Посему эта статья о том, как сделать асинхронное программирование действительно простым. Но добиться настоящей простоты обычно довольно-таки сложно, поэтому, чую, статья получится вполне объёмной.

В обратном порядке, от не очень приятного метода до самого клёвого и независимого (от того, node.js вы используете или браузерный движок), поэтому я вам даже советую просто проскроллить вниз. Кроме того, последние пункты немного разъяснят как работают, например, библиотеки. …И сразу же пример из него, чтобы уж точно никто не смущался: семью строками кода JS, без библиотек, мы добъёмся, например, этого:

successive_read(read_file('a'),                     read_file('b'),                     read_file('не_существует'),                     read_file('c') // тело этой, последней, функции не будет вызвано (вообще!)              ); 

Когда всё это нужно?

  • Цепочки запросов к серверному API
  • Необходимость последовательного извещения UI по сингалам с сервера, да и без сигналов тоже
  • Поочерёдное чтение файлов
  • Парсеры текста и парсер-генераторы
  • Аналоги консольного pipe (|) или направление символьных потоков
  • Чтение пользовательского ввода
  • Безопасные вызовы цепочек функций

Цепочки, цепочки… Вы заметили, да?

Содержание

Путь 1. Просто вызовы

Почти что первое, что приходит на ум.

Допустим, у вас есть серверное атомарное API, есть класс работы с ним и мы хотим получить сортированный список пользователей. Сильно не утруждайтесь разбираться, оцените только размер и сложность кода, это всё равно худший из вариантов (хотя в сети встречаются ещё хуже):

var people = (function() {    var papi = new PeopleAPI();    function People() {     this.__requested = false;     this.__callback = null;   }    People.prototype.getAllSorted = function(callback) {     if (this.__requested) throw new Error('Request already started');     this.__requested = true;     this.__callback = callback;     papi.orderedBy('name', bind(this, this._gotOrder));   }    People.prototype._gotOrder = function(order) {      var res = {}, got = 0;      var got_one = (function(people, count) {        return function(man) {          res[man.id] = man;          people.__got++;          if (people.__got === count) {            people._gotAll(order, res);          }        }      })(this, order.length);      for (var oi = 0, ol = order.length; oi < ol, oi++) {        papi.find(order[oi], got_one);      }   }    People.prototype._gotAll = function(order, res) {     this.__callback(order, res);     this.__requested = false;   }    return new People();   })(); 

В нужный момент мы передаём нужный метод-хэндлер, храним состояние вызова… Ох, всё равно до хрена монструозно, правда? Ужас, ужас! Мне даже сейчас было противно писать это и я ничего не тестировал, хотя когда-то похожим образом у меня был построен относительно крупный проект (там выглядит чуть лучше, потому что API писал тоже я :) ). Пропускаем.

Путь 2. Шины событий

Попробуем быть чуть умнее, заведём общую шину событий:

var handlers = {   'user': {},    'book': {},    'message': {},   '_error': [] // допустим, ошибки не зависят от namespace };  events = [ 'update', 'create', 'delete', 'list' ];  for (var ei = events.length; ei--;) {   for (var ns in handlers) { // мой объект чист, поэтому не нужно `hasOwnProperty`     handlers[ns][events[ei]] = [];   } }; 

Шина событий в данном случае разбита на подразделы (области имён), а глубже уровнем подразделы разбиты на типы событий, где на данный момент содержатся пустые массивы. Например, handlers.user.update и handlers.message.list это пустые массивы ([]), и так для каждого события в каждом подразделе.

Теперь организуем функции подписки на события и ошибки и функции выброса (ну а как ещё назвать?) и тех и других.

// теперь объект handlers можно наполнять ссылками // на "слушателей", группируя их по неймспейсу // и типу события  // подписаться на событие в неймспейсе function subscribe(ns, event, handler) {   handlers[ns][event].push(handler); }  // подписаться на сообщения об ошибках function subscribe_errors(handler) {   handlers._error.push(handler); }          // сообщить о произошедшем в неймспейсе сообытии function fire(ns, event, e) {   var e_handlers = handlers[ns][event],       hname = 'on_'+ns+'_'+event,       handler;   for (var ei = e_handlers.length; ei--;) {     handler = e_handlers[ei][hname];     handler.call(handler, e);   } }  // сообщить о произошедшей ошибке function fire_error(err) {   var e_handlers = handlers._error;   for (var ei = e_handlers.length; ei--;) {     e_handlers[ei].on_error.call(handler, err);   } } 

По сути это весь необходимый код механизма событий и он, по-моему, довольно приличный. Так что, без лишних рассуждений, приведём пример использования:

// некий proxy к серверному API,  // делает только асинхронные вызовы var uapi = new UserAPI();   // ваше приложение function MyApp() {   // TODO: сделать функцию subscribe_all('user', this)   subscribe('user','list', this);   subscribe('user','update', this);   . . .   subscribe_errors(this); } // запросить список пользователей MyApp.prototype.requestUsers = function() {   uapi.get_all(function(order, res) {     fire('user', 'list', {       order: order, list: res     });   }); }; // обновить данные о пользователе  // (может вызываться при отправке формы заполнения профиля) MyApp.prototype.updateUser = function(user) {   uapi.save(user, function(user) {     fire('user', 'update', user);   }, function(err) {     fire_error(err);   }); }; // этот метод будет вызван при срабатывании события user/list MyApp.prototype.on_user_list = function(users) {   . . . // обновление UI   . . . // при необходимости можно выбросить другое событие } // этот метод будет вызван при срабатывании события user/update MyApp.prototype.on_user_update = function(user) {   . . . // обновление UI   . . . // при необходимости можно выбросить другое событие } // этот метод будет вызван при ошибке MyApp.prototype.on_error = function(err) {   . . . // нотификация об ошибке, паника, кони, люди } 

Выглядит значительно более лаконично по сравнению с предыдущим примером и получается даже чем-то похоже на GWT, только в разы короче ;). На события может подписываться не один объект, а сколько угодно, для работы с серверным API — почти что идеальное решение.

Поиграться с ним можно здесь.

Но для парсеров и последовательного чтения файлов — не совсем то. Теперь представим, что нам надоело, мы закрыли глаза, и обратились в сторону библиотек, не задаваясь вопросом что за ними стоит. Просто — взять и вставить, кого нынче волнуют килобайты и внутренности всяких этих хламидомонад?

Путь 3. Библиотеки

Как бы там ни было, по сравнению с другими популярно-предлагаемыми способами, библиотеки — не самое плохое решение. Хоть их и пишут сразу кучу по первой же надобности, некоторые из них отдельно хороши и в разы повышают качество вашего кода. Просто пара ссылок, думаю вы запросто сами разберётесь как их использовать:

Туда же пойдут Dojo.deferred и прочие кандидаты. Плюс, январская презентация «Аsynchronous JavaScript» (англ.) от автора третьей библиотеки вдогонку.

Путь 4. «Чистые» монады

モナダの空道

…Ух ты, почти что ни одного упоминания о монадах в JS на русском, а я надеялся, мне не придётся их объяснять. Впрочем, я и не буду. И не будет в этой главе примеров кода «правильных» монад на JS. Англоязычных статей за последний год тысячи и в ближайшее время кто-нибудь их, да переведёт, и такого кода там завались:

Но долг требует хотя бы вкратце изложить суть.

В разделе «Когда это нужно?» почти весь список содержал популярные примеры применения монад, причём распространено мнение, что вы можете использовать их часто даже сами не осознавая того, что вы их используете. (Знаю я этот приёмчик, слышал не раз). И монады, кстати, стары, как сам программистский мир.

…Однако восклик “ах, блин, да это же монады, я ведь их часто использую”, родился и у меня. Не супер, прямо скажем, часто, но, оказывается, правда случается. И это действительно ещё одна вещь из того множества, которое надо понимать любому уважающему себя программисту.

Примечание: К моему стыду, я очень плохо понимаю код на Haskell и как он работает, даже в двустрочных примерах, хоть и предпринимал пару решительных попыток залезть во вражеский лагерь. С другими языками программирования у меня обычно таких проблем нет (читаю за еду код на Java, Lisp, Python), а вот тут — обнаружилась. Посему мои последующие (до пятой главы) слова отнюдь не аксиомы, а лишь то, что я увидел со своего берега. Я могу даже нагло врать, абсолютно не стесняясь (говорят, правда, что я этого не умею, но в тексте не должно быть заметно), но если вы вчитываетесь в эту главу, другого выхода, кроме как поверить на слово, у вас, на данный момент, нет :)

Всё просто. Если вы задались любой проблемой из вышеупомянутого списка из раздела «Когда это нужно?», значит вам нужны монады. И, как верно для любого паттерна, вы бы рано или поздно к ним пришли.

Они, в каких-то своих проявлениях, находятся среди вас — например, когда вы используете в консоли пайп:

> cat my.js | more 

Достаточно задуматься о том, как этот пайп написан, и вы тут же поймёте монады. Ну, не справочное описание, а именно как они примерно работают.

Если файл my.js не существует, more не будет вызван вообще. Это нам довольно знакомо, мы ведь со времён Perl любим писать:

> read_file('my.js') || die('where's my file?!') 

Основная проблема в написании такого оператора — передача контекста. Вы не хотели бы знать, как работают cat, more, read_file или die (хотя снова вру, иногда очень даже интересно, что там, после этого die…). Вы бы скорее потребовали от них некий общий протокол общения, которому бы они беспрекословно следовали. Что-нибудь такое, что сделало бы очевидным, сорвалась операция или нет и готов для приёма абстрактный поток или не судьба.

Чтобы проблема была видна нагляднее, сделаем цепочку подлиннее, что-нибудь злобное (не пробовать дома, я и сам не пробовал):

> cat my.file >> /dev/dsp >> /dev/hda1 >> my_utility >> /dev/null 

Монада — это и элемент такой цепочки и одновременно функция, которая её обрабатывает.

«Чистая» монада должна быть полностью независимой от внешнего контекста, быть вещью в себе, но от неё требуется вернуть унифицированный ответ. При этом позволяется заставить её возвращать этот самый унифицированный ответ через всяческие функции-обёртки, но, ещё раз, для использования в цепочке необходимо, чтобы каждый элемент был унифицирован, вся цепочка должна работать по единому правилу и её элементы должны быть компонуемы

В нашем, последнем представленном, случае, любой элемент (или сам контекст вызова) должен уметь оборвать выполнение всей цепочки, если хоть один элемент не доступен, и последовательно запрашивать новые куски потока, если всё хорошо. А для обеспечения корректной работы всего этого мы должны понять, произошла ошибка или нет и принудить все элементы передавать поток единообразно.

Вот этот момент, с ошибкой, является характерным примером монады MayBe, которую мы незаметно так рассмотрели: в некоторых языках (JavaScript среди них, so sad ;( ) нет специального типа для ошибки (временно забудем о try/catch) и мы не можем стопроцентно для всех случаев сказать, хотел нам пользователь намеренно вернуть undefined, null или false как некие пустые данные или он правда имел ввиду, что произошла ошибка. В шелле есть exit code и это однозначное сообщение об ошибке, так все эти пайпы и работают. И Хаскель тоже так умеет, а JavaScript вот — нет.

Так что монада — это некая функция, которая может быть вызвана в некой очереди, в дереве процессов или просто независимом контексте и, оставаясь для них прозрачной, способная адекватно сообщить о своём состоянии. Навскидку — так.

В Хаскеле все функции «чисты» и не изменяют что-либо вне себя (в смысле вообще ничего!), они работают исключительно с одним аргументом (другой функцией, каррирование), а кроме этого занимаются только подготовкой возвращаемого значения, и лезть куда-то наружу для них — святотатство. Поэтому почти любую функцию в Хаскеле можно «омонадить» (TODO: спросить Хаскелистов, похоже на правду это утверждение или нет), просто потому что она независима и прозрачна. Так рождаются различные комбинации монад.

Кроме MayBe (привязка точной информации об ошибке к оборачиваемой функции) существуют другие монады-паттерны: Continuation (связывание нескольких функций между собой), Writer (привязка текстовой информации к функции, например логгинга), I/O (спросить пользователя, дождаться ответа из терминала, отреагировать на ответ; или прочитать файл, дождаться когда он будет доступен, прочитать содержимое, закрыть файл), Identity (привязка/подмена информации в возвращаемом значении), State (привязка состояния к функции) и другие (смотрите ссылки в русской статье на википедии и раздел «Ссылки» статьи на английском).

То есть, как результат, несколько функций можно обернуть в Continuation (последовательный вызов) и для обеспечения требуемой унификации, для каждой можно использовать монаду MayBe и как раз получится наш пайп или оператор || / &&.

Поэтому, когда вы делаете асинхронные вызовы (или даже просто последовательные) к серверному коду — вы тоже используете монады.

Когда вы просите одну функцию вызвать другую или несколько, в неком чистом окружении, и ждёте от них ответ — вы используете монады.

(Кстати, пока я искал материал к статье, нашёл всё-таки одно описание на русском (глава 4), которое, к моему приятному удивлению, показало, что я и правда «всё правильно понял», а пример с пайпом, оказывается, вообще стандартен для описания монадических замутов).

Советую заглянуть в статьи по ссылкам в начале главы и посмотреть, как монады надо «правильно» адаптировать в JavaScript. Там, в общем случае, описываются одна-две монады и приводятся три основные функции: bind, переводящая переданную функцию в компонуемую форму (чтобы её можно было использовать в цепочках), unit, обеспечивающая унифицированный формат для вовращаемого значения функции и, опционально, lift, добавляющая к функции необходимые данные, чтобы передавать их по цепочке.

Но ввиду неприспособленности JS к настолько абстрактным понятиям, многие реализации требует своих версий этих функций и значительных усилий над собой, чтобы всё это верно организовать. Может где-то недалеко и пишут уже фреймворк с прямой трансляцией хаскелевских монад на JS и это наверное хорошо.

Но я имею привычку отмечать, что «Жаваскрипту — Жаваскриптовое».

Так что хватит этой напыщенной чистоты, пора и грязь познать :)

Путь 5. «Грязноватые» (но от этого такие простые) монады или «Мы можем это сами»

モナダの土道

Из предыдущего раздела мы узнали что такое монады и как, примерно, они должны «правильно» готовиться. Но, как я люблю говаривать, «Хаскелю — Хаскелево». Монады — общее достояние и каждый язык имеет право смотреть на них со своей колокольни. В статьях, ссылки на которые вы найдёте предыдущей главе, они адаптируются в язык не то чтобы дословно, но довольно тщательно — авторы стремятся дать почти идентичное хаскелевому решение, универсальное для всех функций. На самом же деле это больше концепция, чем необходимость дословной реализации и таскания её за собой.

JS на самом деле не особо предусмотрен для таких инъекций, код становится только толще и сложнее, а таскать за собой ещё парочку js-файлов, раз этого нет в явном виде в стандарте языка, иногда очень даже «не комильфо». Для того чтобы подход стал простым, надо кое-от-чего отказаться.

Откажемся от передачи контекстов, условимся, что контекст у нас внешний и функции могут в него свободно писать. Сначала может показаться, что реализация станет зависимой от задачи, но это совсем не так: наоборот, мы оставим на своё усмотрение операции над контекстом и доверимся одной-единственной функции, которая будет превращать другую, переданную ей, функцию в отложенную и компонуемую. Вот она:

function deferrable(f) { // выберите название поприятнее   return function() {     return (function(f, args) {       return function() { return f.apply(null, args); };     })(f, arguments);   } } 

Я свернул пару строчек в одну, чтобы их правда было семь :).

Это изящное, на мой взгляд, сочетание тех самых bind и unit.

Посмотрим, как можно это использовать. Допустим, мы хотим манипулировать чтением файлов, выполняя его по цепочке и обрывая цепочку, если какой-либо файл из неё не был найден.

/* функция, вызовы к которой мы хотим уметь откладывать */ function read_file(name) {   console.log('reading ' + name);   return name !== 'foo.js'; // true, если имя файла не `foo.js` } 

Заметим, однако, что для этого простейшего случая, мы уже умеем это делать:

/* мы можем эмулировать метод "прервать очередь по падению" через оператор &&                     или метод "прервать по первой удаче" используя оператор ||    но это всё.     обратите внимание, что `read_file('c')` не вызывается. */ read_file('a') && read_file('b') && read_file('foo.js') && read_file('c'); // > reading a // > reading b // > reading foo.js // < false 

Если условиться, что функция возвращает более осмысленное значение (например, ссылку на файл) и null при ошибке (но помните о MayBe), то в JS мы можем сделать даже так:

var found = find_file('foo.js') || find_file('a') || find_file('b'); // > found // < [file 'a'] 

Пояснение бы не было полным, если бы мы не сэмулировали это поведение через функции. Функция, которая эмулирует поведение &&, выглядит примерно так:

/* некая функция, которая оперирует над списком других функций    более хитрым способом     подготавливает их, прерывает, всё что угодно... */ function smart_and() {   var fs = arguments, // массив отложенных функций       flen = fs.length;   for (var i = 0; i < flen; i++) {     // если функция не сработала, остановить процесс     if (!fs[i]()) return;    }; }     

А функция, которая эмулирует поведение ||, выглядит примерно так:

function smart_or() {   var fs = arguments, // массив отложенных функций       flen = fs.length,       res = null;   for (var i = 0; i < flen; i++) {     // если функция сработала, вернуть результат     if (res = fs[i]()) return res;    }; } 

Но если мы захотим использовать одну из них, то нам придётся сделать что-то трудночитаемое:

/* мы можем использовать smart_and таким вот образом,     но выглядит, честно говоря, хреново     да, мы можем обпередаваться внутрь массивами имён файлов    и обрабатывать их внутри, но тогда надо будет назвать её не     smart_and, а скорее smart_read_file */ smart_and(function() { return read_file('a') },            function() { return read_file('b') },           function() { return read_file('foo.js') },            function() { return read_file('c') }); // > reading a // > reading b // > reading foo.js // < undefined 

Вся проблема здесь в подготовке массива отложенных функций. Как в JS можно вызвать функцию, передав ей параметр, запомнив его, но не выполнив её тела до тех пор, пока к ней не было обращения, как это делают ||/&&? Очень просто, она должна вернуть внутреннюю функцию, содержащую своё тело:

function my_func(arg) {   return function() {     console.log(arg);   } } // > var f = my_func(['a', 0]); // > f // < [function] // > f(); // или напрямую: my_func(['a', 0])(); // < [ "a", 0 ] 

Но это не самый приятный подход, оборачивать так каждую функцию быстро надоест и выведет вас из себя… Так вот же, наверху, семистрочное решение всех ваших проблем:

function _log(a) { console.log(a); } _log = deferrable(_log); // > _log('Hi!'); // < [function] // > _log('Mooo!')(); // < Moo! 

Вуаля:

// делаем `read_file` откладываемой read_file = deferrable(read_file);  /* ... достаточно круто, ведь правда?     обратите внимание, что `read_file('c')` не исполняется… */ smart_and(read_file('a'), read_file('b'),            read_file('foo.js'), read_file('c')); // > reading a // > reading b // > reading foo.js // < undefined 

Настало время, однако, представить, что наша задача сложнее и нам нужно, например, передать последний найденный файл в следующую функцию — операторы ||/&& здесь уже совсем не подойдут. А то ведь не очень понятно, зачем мы угулбились в эти странные эмуляции операторов, если всё можно сделать их посредством без лишнего кода. Вовсе не всё, на что способны монады. (И, нужно не забыть рассмотреть неоднозначный момент с ошибками). Итак, передача значения через функции по цепочке:

function cell_1(prev) {   console.log('c1:prev', prev, '->1');   return 1; // если бы мы возвращали ноль,              // то условие ниже посчитало бы, что             // функция не прошла } cell_1 = deferrable(cell_1);  // PS. не создайте случайно глобальную переменную  function cell_2(prev) {   console.log('c2:prev', prev, '->2'); return 2; } cell_2 = deferrable(cell_2);   function cell_3(prev) {   console.log('c3:prev', prev, '->3'); return 3; } cell_3 = deferrable(cell_3);  function cell_err(prev) {   console.log('err:prev', prev, '->err'); return false; // или 0? } cell_err = deferrable(cell_err);  function piped() {    var fs = arguments, // массив отложенных функций        flen = fs.length,        res = null;    for (var i = 0; i < flen; i++) {      // обратите внимание, что вызов происходит внутри,      // в отличие от предыдущих вариантов      if (!(res = fs[i](res)())) throw new Error('NaN!');    }; } 

Проверим, отлично работает:

piped(cell_1(...), cell_2(...) ......) // Эммм...... нет... piped(cell_1, cell_3, cell_2, cell_3, cell_err, cell_1); // А, вот! // c1:prev null ->1 // c3:prev 1 ->3 // c2:prev 3 ->2 // c3:prev 2 ->3 // err:prev 3 ->err // Uncaught Error: NaN! 

Или вот, очень актуальная задача, генератор парсеров с правилами и блекджеком. Такой, чтобы можно было сказать…:

(Я этим как раз сейчас занимаюсь, оптимизирую тут один генератор парсеров, который для сложных синтаксисов выдаёт парсеры на несколько мегабайт JS-кода; и в поисках красоты и справедливости, я и пришёл неожиданно к монадам, поэтому у меня есть готовый симпатичный пример.)

// start = ("a"* / "b") "c" (d:f+ { return d.join(':'); }) // f = "YY" "d"+ // "aaacYYddYYdd" -> [ ["a","a","a"], "c", "YY,d,d:YY,d,d" ]  start = function() { return sequence(                          choise(                            any(match("a")),                            match("b")                          ),                          action(                            label("d", some(rule_f)),                            function() { return d.join(':'); }                           )                       )(); }  rule_f = function() { return sequence(                                match("YY"),                                some(match("d"))();  . . .  console.log(parse("aaacYYddYYdd")); // > [ ["a","a","a"], "c", "YY,d,d:YY,d,d" ] 

(Воистину, монадическое торжество!)

Такие функции вполне могут возвратить и пустые строки и undefined (см. action), которые будут являться вполне полноправным значением и оно не будет значить, что что-то упало, что-то не найдено: просто не совпал элемент, но парсинг-то продолжается.

Ни одна из этих функций не должна выполняться по месту вызова, choise может пропустить последний элемент, если совпал первый, должен иметь возможность остановиться в нужный момент и откатиться назад. В этом коде я использую тот же самый deferrable, который я привёл выше, и это мой единственный молоток.

Все используемые функции парсера не сильно сложнее по коду, чем примеры выше, пара-тройка строк на каждую простую, пять-десять на каждую сложную.

sequence, например, подобно smart_or выше, собирает результаты совпавших функций в массив и возвращает его, благодаря чему переменная результата парсинга (вот этот сложносоставной массив) нигде не определена и блуждает по парсеру до окончания его действия и обретает однозначную сущность только по возвращению из функции parse.

Плюс, эксепшны. Тут они подходят как нельзя кстати. Они обычно занимают кучу кода, а мне важен каждый байт, поэтому я тоже обернул их в функции:

// сообщить об ошибке function failed(expected, found) {   failures.push(expected); // да, failures объявлен извне,                            // мне важны простота и размер кода    throw new MatchFailed(expected, found); }  // подавить ошибку при вызове функции и известить о ней коллбэк,  // если таковой указан function safe(f, cb) {   try { return f();   } catch(e) {     if (e instanceof MatchFailed) {       if (cb) cb(e);     } else { throw e; }   } } 

Именно функция safe подавляет ошибки, брошенные при несовпадении, например, от match, перехватывая их, например, для choise, который при неудаче просто переходит к следующему варианту.

Минус такого подхода в том, что эта самая блуждающая переменная результата при выбросе исключения теряется. Вернее, каждый раз перед потенциальной неудачей, её нужно сохранять (например, передавать в failed). То есть, если вы собираетесь использовать сгенерированный парсер чтобы подсвечивать текст в редакторе (например, маркдаун) на лету, то вы могли бы как раз и опираться на этот эксепшн для сборки табика code completion. Но предыдущий-то код тоже надо подсвечивать, а прошлый результат парсинга мог и устареть.

В общем, с ошибками ситуации изредка и правда могут быть не однозначными: из-за сомнительности возвращаемых типов, из-за сложных структур, которые нужно восстановить при ошибке и т.п. поэтому, в этих редких случаях, приемлемо по-хаскельному примешивать к возвращаемым значениям функций код или инстанс ошибки, например. Тут и понадобится монада MayBe и всяческие bind/unit.

Но вы ведь можете просто чётко знать, чего вы хотите достичь и что происходит в вашем коде и свободно оперировать внешними переменными. Так что, учтите — перебор действий с примешиваниями в JS — это значительная жертва читабельности и простоте кода.

Не замыкайтесь на контекст. Пользуйтесь данной вам свободой. Стремитесь к простоте. Хаскелю — Хаскелево.

Позже, когда закончу, я расскажу про этот парсерогенератор подробнее, а сейчас давно уже пора закругляться, поэтому эпилог:

Эпилог

Ну вот и всё :) Надеюсь, было понятно. Будьте проще! Чмоки-чмоки. xxxxo. また近いうちに

Автор: zokotuhaFly

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js