Elm - это хаскелеподобный чистый функциональный язык для всяких безобразий в браузере, т.е. "компилирующийся" в JavaScript. Знаменит прежде всего тем, что на нем написан визуализатор квантовой механики из предыдущего поста. :)
Три года назад я писал про
тогдашний Elm, с тех пор он заметно изменился, а в последней на сегодня версии произошло существенное изменение в архитектуре, отчего большая часть старого кода и описаний перестала быть актуальной.
Изначально он появился как воплощение идей FRP, и
thesis (дипломную работу?) автора Elm'a я могу всем рекомендовать как замечательное изложение идей и разных подходов к FRP, плавно переходящее в объяснение исходной архитектуры Elm'а (и без этого объяснения научиться тогдашнему Elm'у было трудно). Там вся динамика была построена на идее сигналов, когда есть обычные иммутабельные данные типов A,B,C... и есть отдельная категория типов A',B',C'... описывающих такие же данные, но меняющие свои значения со временем (навроде Time -> A'), и есть функтор Signal из первой категории во вторую. Пишешь чистый код, работающий с простыми иммутальными данными, потом этим функтором лифтишь свои чистые функции в мир динамически меняющихся значений. Есть набор внешних источников событий/данных, вроде координат мыши, т.е. уже живущих во второй категории, и нужно построить в ней функцию из нужных источников событий/данных в некое дерево контролов, элементов. А рантайм уже сам позаботится о том, чтобы все события и новые данные проходили как надо через все преобразования и получающееся на выходе дерево превращалось в DOM страницы. Ну и были во второй категории специальные комбинаторы для обращения с временнЫми потоками данных, вроде соединения, сворачивания и пр.
Потом в Elm'е появились Mailbox'ы и Elm Architecture, в которой программа описывалась двумя функциями view и update и начальным значением пользователького типа Model (содержащего все данные). update получала значение произвольного заданного пользователем типа Action (обычно это перечисление разных действий) и текущее значение модели, возвращала обновленное значение модели, а view отображала значение модели в дерево элементов, принимая одним из параметров "адрес" (значение специального типа Mailbox). Возвращаемые ф-ей view элементы в своих атрибутах могли иметь функции "что делать при нажатии/изменении", эти функции получали тот самый "адрес", чтобы слать свои оповещения туда. Так все оставалось иммутабельным и чистым, а рантайм заботился о доставке всех событий в форме пользовательского типа Action в функцию update, так осуществлялся круговорот событий-реакций в колесе сансары. Как видите, в явном виде сигналы уже не участвовали в Elm Architecture, но в разных API еще оставались.
В свежей версии 0.17 авторы сказали "прощай FRP" и выкинули все сигналы нафиг.
И API для построения дерева элементов поменяли заодно. Зато добавили модный способ работы с первоклассными эффектами, как у взрослых. Осталась Elm Architecture, но уже другая. Теперь программа это
program : { init : (model, Cmd msg),
update : msg -> model -> (model, Cmd msg),
subscriptions : model -> Sub msg,
view : model -> Html msg } -> Program Never
Т.е. описываешь свой тип Model с любыми нужными данными, описываешь свой тип сообщений (как Action раньше), и три функции: view, update и subscriptions. view тупо отображает модель в DOM HTML, но тип дерева элементов параметризован твоим типом сообщений, ибо в атрибутах элементов вставляются функции реакций на события, которые производят значения этого самого типа твоих сообщений. Им теперь не нужно знать ни про какие мэйлбоксы, не нужно туда ничего слать, просто произвести сообщение, рантайм сам знает куда его доставлять. Кроме того есть функция subscriptions, которая исходя из текущего состояния модели говорит, какие внешние события нам интересно слушать, и тип ее ответа тоже параметризован типом наших сообщений, т.к. внешние события приходят в рамках все того же потока сообщений, а когда подписываешься на внешнее событие, говоришь, как его завернуть в твой тип сообшений. Ну и ф-я update, которая получает все эти сообщения твоего типа и меняет модель, а заодно может произвести "команду" - указание рантайму произвести некоторый эффект, подобно значению типа IO Smthng в хаскеле. Выражение действий в виде данных обещает много бенефитов, но я в эту тему пока не вдавался.Вот такой теперь Elm, больше никаких сигналов, но по-прежнему все чисто функционально.
В версии 0.17 зачем-то изменился синтаксис описания модулей, там мелочь поменялась, но из-за нее некоторые библиотеки не собираются, надо одну строчку поменять в заголовке модуля.
А еще в Elm'e неожиданно оказалась встроенная поддержка WebGL: там не просто есть нужная библиотека, а компилятор умеет распарсить текст шейдеров на GLSL и проверить согласованность используемых типов между шейдерами и основной программой! Во-первых, сама библиотека для работы с WebGL более высокоуровневая и намного более удобная в обращении, чем то, что я видел в примерах про WebGL на JavaScript'e. В JS, похоже, просто копировали Си, там даже указатели есть, и приседаний на каждый чих нужно не меньше дюжины. В элмовском модуле WebGL все это безобразие спрятано, а выставлен довольно чистый и удобный API. Программа на Elm'e производит HTML дерево, соответственно конечная точка в WebGL это
toHtml : List (Attribute msg) -> List Renderable -> Html msg
и наша задача произвести список Renderable, которые получаются так:
render
: Shader attributes uniforms varyings
-> Shader {} uniforms varyings
-> Drawable attributes
-> uniforms
-> Renderable
где
type Drawable attributes
= Triangle (List (attributes, attributes, attributes))
| Lines (List (attributes, attributes))
| LineStrip (List attributes)
...
Т.е. кусок графики (Renderable) получается из четырех вещей: двух шейдеров (вершин и пикселей), описания геометрии и общих данных для шейдеров. Причем и геометрия, и общие данные описываются пользовательскими типами - что хочешь, то туда и передавай. Например, набор треугольников представлен списком троек, но троек чего? Чего скажешь: хошь координат, хошь более сложных структур. Вертексный шейдер получит значения из этих списков в виде атрибутов - данных, меняющихся от вершины к вершине, а в качестве uniform данных (общих для всех вершин) получит то, что передашь, тут тоже твой тип, сам решаешь. А как в render передать шейдер? Это значение типа Shader attributes uniforms varyings (где все три типа-параметра - твои, какие скажешь), и описывается значение шейдера специальным синтаксисом с текстом шейдера на GLSL. Компилятор посмотрит, какие данные в тексте шейдера описаны как attribute, uniform и varying, и убедится, что они соответствуют полям твоих типов для attributes, uniforms и varyings, что переданы параметрами типу Shader. Тут происходит связь города и деревни, связь кода на Elm и кода на GLSL. Пример из моей программы:
sphVertSh : Shader { attr | position:Vec3, color:Vec3 } { unif | perspective:Mat4, pos:Vec3 } { v:Vec3 }
sphVertSh = [glsl|
attribute vec3 position;
attribute vec3 color;
uniform mat4 perspective;
uniform vec3 pos;
varying vec3 v;
void main () {
gl_Position = perspective * vec4(position*0.1 + pos, 1.0);
v = position;
}
|]
Такой вершинный шейдер будет вызван для каждой вершины сферы, он получит данные о вершине в виде attribute vec3 position и color, вычислит требуемое значение позиции и заодно запишет что нужно в выходное значение varying v, которое потом пиксельный шейдер получит проинтерполированным себе на вход и будет использовать для вычисления цвета пикселя. Поскольку оба вершинный и пиксельный шейдер передаются вместе в render, типы их uniforms и varyings обязаны совпадать, таким образом компилятор проверит не только то, что я в шейдеры правильные данные передаю, но и что вершинный шейдер производит именно такие данные, которые принимает пиксельный. При том, что тип этих данных я придумываю сам. Если сравните это с JS'ным WebGL или сишным OpenGL, увидите пропасть как в удобстве, так и в уровне статического контроля. Там все намного труднее делается и с минимумом проверок.
Другие впечатления после написания полтыщи строк. Близкий к хаскелю синтаксис и отличный вывод типов в сочетании с referential transparency от чистоты и иммутабельности позволяют очень легко массировать код: любой кусок можно элементарно превратить в функцию, перенести в другое место, параметризовать, при этом не порождается бойлерплейта и впечатления от процесса очень положительные. Компилятор очень шустрый (единственная быстрая программа на хаскеле из мне известных), сообщения об ошибках более чем подробные и ласковые, об этом авторы особенно заботились. В синтаксисе нет where, и это очень хорошо, на мой взгляд. По-прежнему есть что-то вроде встроенных тайпклассов (вроде number), но нельзя описывать свои тайпклассы или добавлять свои инстансы, когда работаешь с комлексными числами об этом сильно жалеешь. Можно описывать свои операторы, но только на самом внешнем уровне, т.е. определить оператор локально (чтобы было замыкание, задействовать значения из текущего скоупа) нельзя, это тоже обидно немножко.
Но общие впечатления очень положительные. Язык стал проще и одновременно удобнее. Компилятор стал стабильнее и вообще образцовый во многих отношениях. Есть очень классный менеджер пакетов и их репозиторий (причем они умеют гарантировать соблюдение логики semver, это отдельная тема). Есть удобная штука elm reactor, навроде debug mode в рельсах, когда поменял исходник, нажал в браузере F5 и все пересобралось и перезапустилось, благо компилируется мгновенно. А еще там удобные record'ы с row polymorphism'ом! О чем еще мечтать? :)