Статьи

Функциональное реактивное программирование с помощью Elm: введение

Эта статья была рецензирована Морицем Крегером , Марком Брауном и Дэном Принсом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

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

В настоящее время основной задачей Elm является упрощение и надёжность разработки интерфейса. Elm компилируется в JavaScript, поэтому его можно использовать для создания приложений для любого современного браузера.

Вяз — это статически типизированный язык с выводом типа . Вывод типа означает, что нам не нужно объявлять все типы самостоятельно, мы можем позволить компилятору выводить многие типы для нас. Например, записав one = 1 , компилятор узнает, что one является целым числом.

Elm — это почти чистый функциональный язык программирования. В основе Elm лежит множество функциональных шаблонов, таких как чистые представления , ссылочная прозрачность , неизменные данные и контролируемые побочные эффекты . Он тесно связан с другими языками ML, такими как Haskell и Ocaml .

Вяз реагирует. Все в Эльме течет через сигналы . Сигнал в вязе переносит сообщения со временем. Например, нажатие на кнопку отправит сообщение поверх сигнала.

Вы можете думать, что сигналы похожи на события в JavaScript, но в отличие от событий, сигналы являются первоклассными гражданами в Elm, которые можно передавать, преобразовывать, фильтровать и комбинировать.

Вяз Синтаксис

Синтаксис Elm напоминает Haskell , так как оба являются языками семейства ML.

 greeting : String -> String greeting name = "Hello" ++ name 

Это функция, которая принимает String и возвращает другую String .

Зачем использовать вяз?

Чтобы понять, почему вы должны заботиться о Elm, давайте поговорим о некоторых тенденциях внешнего интерфейса в последние пару лет:

Опишите состояние вместо преобразования DOM

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

Вместо того чтобы делать это, такие библиотеки, как React , популяризировали идею сосредоточения внимания на описании определенного состояния DOM, а затем позволяли библиотеке обрабатывать преобразования DOM для нас. Мы сосредоточены только на описании дискретных состояний DOM, а не на том, как мы туда доберемся.

Это приводит к существенно меньшему количеству кода для написания и поддержки.

События и преобразование данных

Когда дело доходит до состояния приложения, обычным делом является изменение состояния самостоятельно, например, добавление комментариев в массив.

Вместо того, чтобы делать это, мы можем только описать, как состояние приложения должно меняться в зависимости от событий, и позволить другому применить эти преобразования для нас. В JavaScript Redux сделал популярным этот способ создания приложений.

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

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

Однонаправленный поток данных

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

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

Неизменные данные

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

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

Централизованное государство

Еще одной тенденцией в развитии внешнего интерфейса является использование централизованного «атома» для сохранения всего состояния. Это означает, что мы помещаем все состояния в одно большое дерево, а не разбрасываем его по компонентам.

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

Чистые компоненты

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

Это делает понимание и тестирование наших компонентов намного проще, чем раньше, поскольку они более предсказуемы.

Вернуться к вязу

Все это отличные шаблоны, которые делают приложение более надежным, предсказуемым и обслуживаемым. Однако, чтобы правильно использовать их в JavaScript, мы должны быть осторожны, чтобы не делать некоторые вещи в неправильных местах (например, изменять состояние внутри компонента).

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

В Elm мы создаем приложения, используя:

  • Неизменные данные
  • Чистые взгляды, которые описывают DOM
  • Однонаправленный поток данных
  • Централизованное государство
  • Централизованное место, где описаны мутации данных
  • Содержатся побочные эффекты

безопасности

Другая большая выгода вяза — безопасность, которую это обеспечивает. Полностью избегая возможности значения NULL, это заставляет нас обрабатывать все альтернативные пути в приложении.

Например, в JavaScript (и многих других языках) вы можете получить ошибки времени выполнения, выполнив что-то вроде:

 var list = [] list[1] * 2 

Это вернет NaN в JavaScript, который вы должны обработать, чтобы избежать ошибки во время выполнения.

Если вы попробуете нечто подобное в Elm:

 list = [] (List.head list) * 2 

Компилятор отклонит это, сообщив, что List.head list возвращает тип Maybe . Тип Maybe может содержать или не содержать значение, мы должны обработать случай, когда значением является Nothing .

 (Maybe.withDefault 1 (List.head list)) * 2 

Это дает нам большую уверенность в наших приложениях. Очень редко можно увидеть ошибки времени выполнения в приложениях Elm.

Образец заявки

Чтобы получить более четкое представление о языке Elm и о том, как с его помощью создаются приложения, давайте разработаем крошечное приложение, которое отображает HTML-элемент, перемещающийся по странице. Вы можете попробовать это приложение, перейдя по адресу http://elm-lang.org/try и вставив туда код.

 import Html import Html.Attributes exposing (style) import Time name : Html.Html name = Html.text "Hello" nameAtPosition : Int -> Html.Html nameAtPosition position = Html.div [ style [("margin-left", toString position ++ "px")] ] [ name ] clockSignal : Signal Float clockSignal = Time.fps 20 modelSignal : Signal Int modelSignal = Signal.foldp update 0 clockSignal update : Float -> Int -> Int update _ model = if model > 100 then 0 else model + 1 main : Signal Html.Html main = Signal.map nameAtPosition modelSignal 

Давайте рассмотрим это по частям:

 import Html import Html.Attributes exposing (style) import Time 

Сначала мы импортируем модули, которые нам понадобятся в приложении.

 name : Html.Html name = Html.text "Hello" 

name — это функция, которая возвращает Html элемент, содержащий текст Hello .

 nameAtPosition : Int -> Html.Html nameAtPosition position = Html.div [ style [("margin-left", toString position ++ "px")] ] [ name ] 

nameAtPosition оборачивает name в тег div . Html.div — это функция, которая возвращает элемент div . Эта функция занимает целочисленную position в качестве уникального параметра.

Первый параметр Html.div — это список атрибутов HTML. Второй параметр — это список дочерних элементов HTML. Пустой тег div будет Html.div [] [] .

style [("margin-left", toString position ++ "px")] создает атрибут стиля HTML, который содержит margin-left с заданной позицией. Это закончится как style="margin-left: 11px;" когда вызывается с позиции 11 .

Таким образом, в итоге nameAtPosition отображает Hello с полем слева.

 clockSignal : Signal Float clockSignal = Time.fps 20 

Здесь мы создаем сигнал, который передает сообщение 20 раз в секунду. Это сигнал поплавков. Мы будем использовать это как пульс для обновления анимации.

 modelSignal : Signal Int modelSignal = Signal.foldp update 0 clockSignal 

clockSignal дает нам пульс, но сообщения, которые он посылает через сигнал, бесполезны, полезная нагрузка clockSignal — это просто дельта между каждым сообщением.

Что нам действительно нужно, так это счетчик (то есть 1, 2, 3 и т. Д.). Для этого нам нужно сохранить состояние в нашем приложении. То есть возьмем последний отсчет, который у нас есть, и увеличиваем его каждый раз, clockSignal срабатывает clockSignal .

Signal.foldp — это то, как вы сохраняете состояние в приложениях Elm. Вы можете думать о foldp аналогично Array.prototype.reduce в JavaScript, foldp принимает функцию накопления , начальное значение и исходный сигнал .

Каждый раз, когда исходный сигнал передает событие, foldp вызывает функцию накопления с предыдущим значением и сохраняет возвращаемое значение.

Таким образом, в этом случае, каждый раз, когда clockSignal передает сообщение, наше приложение вызывает update с последним счетом. 0 является начальным значением.

 update : Float -> Int -> Int update _ model = if model > 100 then 0 else model + 1 

update является функцией накопления . В качестве первого параметра он принимает значение Float являющееся дельтой из clockSignal . Целое число, которое является предыдущим значением счетчика в качестве второго параметра. И возвращает другое целое число, которое является новым значением счетчика.

Если model (предыдущее значение счетчика) больше 100, мы сбрасываем ее на 0, в противном случае просто увеличиваем на 1.

 main : Signal Html.Html main = Signal.map nameAtPosition modelSignal 

Наконец, каждое приложение в Elm начинается с main функции. В этом случае мы map modelSignal мы создали выше, через функцию nameAtPosition . То есть, каждый раз, когда modelSignal передает значение, мы повторно визуализируем представление. nameAtPosition будет получать полезную нагрузку от modelSignal качестве первого параметра, эффективно изменяя стиль margin-left элемента div двадцать раз в секунду, поэтому мы можем видеть текст, перемещающийся по странице.

Приложение, которое мы только что создали, демонстрирует:

  • HTML в вязе
  • Использование сигналов
  • Сохраняя состояние функциональным образом
  • Чистые взгляды

Если вы использовали Redux, вы заметите, что есть несколько параллелей между Elm и Redux. Например, update в Elm очень похоже на редукторы в Redux. Это потому, что Redux был сильно вдохновлен архитектурой Elm .

Вывод

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

Кривая обучения тому, как структурировать приложение Elm, не тривиальна, поскольку приложения, использующие функциональное реактивное программирование, отличаются от того, к чему мы привыкли, но они того стоят.

Дополнительные ресурсы

  • При создании больших приложений в Elm рекомендуется использовать архитектуру Elm. Смотрите этот учебник для получения дополнительной информации.
  • Сообщество Elm Slack — отличное место, чтобы обратиться за помощью и советом.
  • Видеоролики Pragmatic Studio о Elm — отличный ресурс для начала работы.
  • Elm-tutorial — это руководство, над которым я работаю, чтобы научить создавать веб-приложения с помощью Elm.