Начало проекта на 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

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

main : Signal Element
main = scene <~ state ~ Window.dimensions

Театр начинается с вешалки, а приложение с main. И сразу бросается в глаза тип: Signal Element. “Но где же IO?”, спросит невнимательный читатель, пропустивший вступление, и будет прав. Elm ещё более ортодоксальный чем Haskell в плане побочных эффектов и вся обработка идёт чистыми функциями.

Забавные стрелочки такую обработку как раз задают. Читается это как "С помощью функции scene сведём значения сигналов state и Window.dimensions результатом такой обработки будет элемент приложения (в этой статье нам не интересный). Всё это наводит на мысли об апликативных функторах и не зря. Но Elm выше всего этого теорката.

scene : State -> (Int, Int) -> Element
scene state (w,h) = Html.toElement w h (view state)

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

state : Signal State
state =
    foldp update startingState
    <| mergeMany [ Signal.subscribe actions ]

А тут мы имеем дело уже с этим самым потоком. У него нет входов и выходов и это, наверно, самый мутный участок во всём коде. Этот сигнал агрегируется из нескольких (пока что только одного), затем передаётся в функцию “свёртки из прошлого” foldp. Как и в обычном фолде, нам потребуется начальное значение и функция, которая будет обновлять аккумулятор в ответ на входящие данные.

view : State -> Html
view state =
    p [] [ text state.message ]

Теперь пришла пора описать вторую часть сцены. Я не стал городить хитрые и сложные интерфейсы на бустрапе, а просто вывел один элемент <p> без атрибутов и с содержимым поля message нашего стейта в качестве текстового тела HTML-элемента. Никаких императивных манипуляций, только чистые комбинаторы из модуля Html и списки, списки, списки…

type alias State =
    { message : String
    }

startingState =
    { message = "Hellow, orld!"
    }

Всё состояние приложения кладётся в один большой “корневой” контейнер. В Elm для этого удобнее использовать не ADT, а рекорды а-ля объекты JS. Это делать совершенно не страшно т.к. это всё же настоящий тип, а не просто словарь-свалка для значений в произвольном формате, так что обратиться к несуществующему полю или полю ненадлежащего типа компилятор вам не даст.

type Action = NoOp

actions : Signal.Channel Action
actions = Signal.channel NoOp

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

update : Action -> State -> State
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. Удачи!