Последние несколько месяцев я искал приключений, изучая все, что мог, о функциональном программировании. Кажется, что каждую неделю появляется новый фреймворк или библиотека внешнего интерфейса, вдохновленная функциональным программированием, поэтому я решил проверить это. Одно из моих исследований было посвящено Elm: удивительному языку, вдохновленному Haskell, для создания веб-приложений. Это дало мне представление о том, что на самом деле означает функциональное программирование. Что действительно привлекло меня к Elm, так это путешествующий во времени отладчик и потрясающая сила абстракции Elm Architecture Tutorial.

Мне потребовалось время, чтобы понять язык вяз (если вы хотите узнать больше, я настоятельно рекомендую этот бесплатный онлайн-класс, в котором преподается Haskell). Но поскольку я не совсем бегло говорил на вязе, мне было трудно по-настоящему реализовать свои идеи. Поэтому подумал: Я попробую проверить свое понимание архитектуры Вяза, реализовав те же концепции в Javascript. Если вы раньше использовали React, вы, вероятно, знакомы с Redux, который на самом деле был вдохновлен Elm, так что, надеюсь, вы заметите некоторые сходства.

Следующий код доступен в моем репозитории elmish github.

Базовый счетчик

В Elm все чисто функционально. Компонент моделируется как конечный автомат с использованием 3 функций:

init   :: () => state
update :: (state, action) => state
view   :: (dispatch, state) => html

Функция обновления - это редуктор . Он получает текущее состояние и действие и создает следующее состояние, которое отображается с помощью виртуальной библиотеки DOM, такой как React. Чтобы отправить действия для обновления состояния, вам просто нужно вызвать диспетчеризацию с действием. Вот и все! Позвольте мне показать вам простой встречный пример:

Теперь все, что осталось сделать, это соединить части. Нам нужно получить начальное состояние компонента и отрендерить компонент с помощью функции просмотра. Теперь с функцией диспетчеризации непросто. Функция диспетчеризации должна обновить состояние, а затем повторно выполнить рендеринг. Это неприятный фрагмент кода, но я думаю, что на него стоит взглянуть и понять:

Еще немного функционального кода

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

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

Потоки похожи на ленивые массивы. Вы можете отображать, фильтровать и сокращать (называемые сканированием) их, и вы можете передавать им значения, просто вызывая их как функции. Это также соглашение, чтобы отслеживать, какие переменные являются потоками, добавляя суффикс $.

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

Затем с потоком состояний мы сопоставляем функцию просмотра, чтобы получить поток виртуальных деревьев DOM. Но мы сделали там кое-что изящное. Мы частично применяем функцию view с потоком действий. Этот доклад поможет вам лучше понять каррирование, но суть в том, что когда вы каррируете функцию и вызываете ее с аргументами, она будет продолжать возвращать функцию до тех пор, пока не получит все аргументы, необходимые функции. По сути, это более обобщенная концепция частичного применения. Здесь важно понять, что функция диспетчеризации - это просто способ ввода значений обратно в поток действий.

Хватит, вернемся к Эльмишу.

Компонентный состав

Одним из преимуществ построения компонентов с использованием шаблона init-update-view является композиция. Возможно, вы захотите найти некоторые из функций Ramda, если вы с ними не знакомы, но прелесть Ramda в том, что, как только вы ее поймете, этот код должен читаться очень декларативно:

Компонент listOf - это компонент более высокого порядка, который создает списки других компонентов. Чтобы вставить элемент, мы просто назначаем элементу идентификатор и инициализируем состояние подкомпонента. Другой важный момент заключается в том, что мы оборачиваем дочерние действия в специальный тип: «child», передавая функцию childDispatch дочерней функции view, которая перенаправляет действия в отправку. Таким образом, в функции обновления мы можем найти дочерний элемент, которому принадлежит действие, по идентификатору, и обновить состояние этого дочернего элемента с помощью функции обновления дочернего элемента.

Это может показаться немного многословным, и я согласен, что это так, но это цена, которую мы платим за удивительную силу абстракции. Если нам нужен список счетчиков сейчас, все, что нам нужно сделать, это:

start(listOf(counter))

Что действительно показывает возможности абстракции, так это то, что вы можете тривиально создать список из списка счетчиков.

start(listOf(listOf(counter)))

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

Отменяемые компоненты

Мой любимый пример, демонстрирующий силу абстракции Elm Architecture, - это отключаемый компонент. Он обертывает компонент и отслеживает историю состояний, так что вы можете отменить и повторить всю историю. И снова этот код для меня очень похож на спецификацию. Отменить - это просто уменьшение времени. Повторить увеличивает время. А дочернее действие означает принятие текущего состояния, обновление его для получения следующего состояния и присоединение его к прошлому при увеличении времени:

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

start(undoable(listOf(counter)))

Вуаля! Просто работает, и безупречно.

Заключение

Так что это пока что. Elm Architecture - это действительно крутой паттерн, который дает вам волшебную силу абстракции через функциональную композицию. Нетрудно представить, как создать отладчик, путешествующий во времени, если взять компонент, который нельзя отключить, и поместить ползунок в нижнюю часть экрана.

Две другие функции, которые действительно меня восхищают, - это воспроизводимые отчеты об ошибках и автоматическое тестирование. Если вы столкнулись с исключительной ситуацией во время выполнения, почему бы не отправить на сервер начальное состояние и все действия, предпринятые пользователем, чтобы вы могли точно воспроизвести исключение? А поскольку все эти функции являются чистыми, вы можете записывать все действия и состояния, когда используете приложение, для создания тестовых примеров, просто щелкнув и используя приложение!

К сожалению, у этого подхода есть пара недостатков, которые я заметил до сих пор:

  • Существует досадное количество шаблонов, необходимых для обертывания дочерних действий. Я думаю, что Clojurescript - интересный вариант для облегчения этой боли из-за его невероятно мощных макросов, но я не собираюсь делать это в ближайшее время. Возможно, есть какая-то абстракция Javascript, о которой я еще не подумал.
  • Все дерево DOM пересчитывается после каждого действия. Сообщество Elm скажет вам, что «преждевременная оптимизация - это корень всех зол», и, хотя я не возражаю, я думаю, что в конечном итоге это станет проблемой, если вы запускаете много анимаций в цикле действия-обновления. Эти компоненты Elmish можно обернуть в компоненты React, чтобы ввести ленивую оценку. Но, например, в компоненте listOf вы по-прежнему будете передавать новую связанную функцию дочернему компоненту при каждом обновлении, заставляя повторно отрисовывать каждый элемент в списке. Было бы замечательно, если бы существовал какой-то стандарт для вывода равенства связанных чистых функций. То есть:
const add = (a,b) => a + b
const f1 = bind(add, 1)
const f2 = bind(add, 1)
// ideally
f1 === f2
// pragmatically
f1.eq(f2)

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

А пока все эти и другие примеры вы можете увидеть в моем Эльмиш репо. Пожалуйста, дайте мне теперь то, что вы думаете! И не стесняйтесь создавать тикеты, если у вас есть какие-либо вопросы, идеи или просто хотите обсудить.