Начало проекта на Elm
date = fromGregorian 2015 jan 22
category = "Elm"
tags = ["FRP", "Elm"]
Что есть Elm?
Elm это язык и платформа для создания приложений, поведение которых следует определённым правилам, призванным бороться с багами и плохим кодом.
Приложение в Elm представляет собой набор чистых данных (никакого IO
!) и приводится в движение “сигналами” которые приходят из “каналов”.
Подобный “спартанский” минимализм позволяет собирать большие приложения из простых компонентов и деплоить их в пятницу вечером.
Синтаксис чем-то похож на Haskell, но имеет сильный привкус ML. Вместо ::
для типов используется :
, а для списков наоборот. Кроме того, тип списка так и выглядит - List a
(начиная с версии 0.14). Композиция функций это f >> g
, а применение это f |> g
. Монадок нет вообще.
Установка расписана на сайте и сводится, как обычно, к скачиванию бинарей или установке с Hackage. Особые эстеты монут собрать свой собственный Elm с гита.
Покажи нам код
В силу определённых причин, при старте нового проекта нужно сделать довольно много телодвижений. Этот пост является своего рода чеклистом и обзором основных сущностей в типичном приложении с использованием HTML интерфейса. Вырезки кода можно собрать в одном файле и он будет рабочим. Чтобы разговор был предметным, сначала будет идти блок кода, а потом описание.
import Dict
import Debug
import Graphics.Element (Element)
import Graphics.Input as Input
import Html
import Html (..)
import Html.Attributes
import Html.Attributes (..)
import Html.Events (..)
import Http
import Maybe
import List
import List ((::), map, concat, filter)
import Result
import String
import Signal
import Signal (Signal, foldp, mergeMany, (<~), (~))
import Window
Первым делом идёт внушительных размеров бойлерплейт. Некоторые импорты не нужны для старта, но понадобятся практически сразу в ходе работы.
: Signal Element
main = scene <~ state ~ Window.dimensions main
Театр начинается с вешалки, а приложение с main
. И сразу бросается в глаза тип: Signal Element
. “Но где же IO?”, спросит невнимательный читатель, пропустивший вступление, и будет прав. Elm ещё более ортодоксальный чем Haskell в плане побочных эффектов и вся обработка идёт чистыми функциями.
Забавные стрелочки такую обработку как раз задают. Читается это как “С помощью функции scene
сведём значения сигналов state
и Window.dimensions
результатом такой обработки будет элемент приложения (в этой статье нам не интересный). Всё это наводит на мысли об апликативных функторах и не зря. Но Elm выше всего этого теорката.
: State -> (Int, Int) -> Element
scene = Html.toElement w h (view state) scene state (w,h)
Эта функция является всего лишь тонкой прокладкой, которую обещают убрать в следующих версиях. Само же текущее состояние системы передаётся функции view
, которая рисует по нему виртуальное DOM-дерево. Обратите внимание, что Signal
пропал из типа и никаких, поэтому в аргументе state
будет уже само значение на текущий момент, а не поток, из которого это значение можно получить.
: Signal State
state =
state
foldp update startingState<| mergeMany [ Signal.subscribe actions ]
А тут мы имеем дело уже с этим самым потоком. У него нет входов и выходов и это, наверно, самый мутный участок во всём коде. Этот сигнал агрегируется из нескольких (пока что только одного), затем передаётся в функцию “свёртки из прошлого” foldp
. Как и в обычном фолде, нам потребуется начальное значение и функция, которая будет обновлять аккумулятор в ответ на входящие данные.
: State -> Html
view =
view state .message ] p [] [ text state
Теперь пришла пора описать вторую часть сцены. Я не стал городить хитрые и сложные интерфейсы на бустрапе, а просто вывел один элемент <p>
без атрибутов и с содержимым поля message
нашего стейта в качестве текстового тела HTML-элемента. Никаких императивных манипуляций, только чистые комбинаторы из модуля Html
и списки, списки, списки…
type alias State =
: String
{ message
}
=
startingState = "Hellow, orld!"
{ message }
Всё состояние приложения кладётся в один большой “корневой” контейнер. В Elm для этого удобнее использовать не ADT, а рекорды а-ля объекты JS. Это делать совершенно не страшно т.к. это всё же настоящий тип, а не просто словарь-свалка для значений в произвольном формате, так что обратиться к несуществующему полю или полю ненадлежащего типа компилятор вам не даст.
type Action = NoOp
: Signal.Channel Action
actions = Signal.channel NoOp actions
Приложение сейчас тривиальное, поэтому никаких действий совершать нельзя. Но канал должен всегда иметь начальное значение.
: Action -> State -> State
update =
update action state case action of
NoOp -> state
В ответ на просьбу сделать ничего функция вернёт оригинальное состояние без изменений.
Запуск
Если запустить elm-reactor
в каталоге с файлом приложения и открыть в браузере его страницу… произойдёт ошибка - надо сначала доставить зависимостей:
$ elm-package install evancz/elm-html
To install evancz/elm-html I would like to add the following
dependency to elm-package.json:
"evancz/elm-html": "1.1.0 <= v < 2.0.0"
May I add that to elm-package.json for you? (y/n) y
Some new packages are needed. Here is the upgrade plan.
Install:
elm-lang/core 1.1.0
evancz/elm-html 1.1.0
evancz/virtual-dom 1.2.0
Do you approve of this plan? (y/n) y
Downloading elm-lang/core
Downloading evancz/elm-html
Downloading evancz/virtual-dom
Packages configured successfully!
После чего вы сможете увидеть заветный приветмир и попробовать добавить элементов в view
, новых полей в State
и событий в Action
. Удачи!