Надысь побаловался Elm'ом - хаскелеподобным языком, компилирующимся в JavaScript (не писать же руками на этом странном языке). Вспомнил, где можно его применить - у меня на странице одного из продуктов было слайд-шоу, наскоро сделанное на флеше с использованием Flex'a, отчего простая довольно флешка весила 280 КБ. Исходников той флешки сейчас под рукой нет, видать остались на старом ноуте, так что сравнить объем не могу, но на Elm'e все уместилось в несколько строк, широко размазанных для пущей вящести. Вот они, с пояснениями.
Сперва идет список имен файлов с картинками, которые будут показываться в цикле, их размер и количество. Сценарий таков: картинки сменяют друг друга не резко, а плавным кросс-фейдом. 4 секунды показываем одну картинку, потом в течение секунды идет переход к следующей. Достигается это наличием двух картинок одна под другой, первые 4 секунды верхняя картинка абсолютно прозрачна, потом ее непрозрачность плавно увеличивается от 0 до 1. По истечении 5 секунд верхняя картинка становится нижней, а верхней становится следующая. Заведем список пар имен картинок ((1,2), (2,3), (3,4) ...).
Типы выглядят как хаскельные, только для объявления типа значения используется одно двоеточие, как в ML и Idris'e, а не два, как в хаскеле. nth - вспомогательная функция получения n-ного элемента списка, в стандартную библиотеку Elm'a ее забыли включить.
Происходящее на экране в нашем случае зависит только от времени. Разобьем пятисекундный интервал на 100 тиков, и будем вычислять параметры (прозрачность верхней картинки и номер пары) по номеру тика:
Отображаемый на экране контент в Elm'е имеет тип Element, и функция построения сцены по тику выглядит так:
Тут функция image (из стандартной библиотеки) из размеров и имени картинки делает отображаемый элемент-картинку, функция opacity меняет непрозрачность переданного ей элемента, а layers берет список элементов и располагает их на одном месте в разных слоях, от дальнего к ближнему.
Теперь у нас есть чистая функция из номера тика в отображаемый элемент, осталось добавить динамики. Динамика в Elm'e построена на принципах FRP (Functional Reactive Programming). Вместо привычных событий и их обработчиков тут есть функтор Signal, отображающий простые чистые типы в типы значений, зависящих от времени. Логически "Signal a" можно считать функцией "Time -> a". Вместо таймера у нас будет целочисленное значение "номер тика", меняющееся во времени, потому имеющее тип Signal Int:
Функция every возвращает значение типа Signal Float, которое обновляется через заданный интервал времени, а count превращает произвольный сигнал в Signal Int - подсчитывая число "обновлений", т.е. возвращая как раз номер тика.
Имея изменяющееся значение типа Signal Int и чистую функцию из Int в Element, мы можем применить fmap функтора, который тут называется lift, чтобы получить значение типа Signal Element - т.е. контент, изменяющийся во времени. Это и станет итоговой программой:
Вот и весь код. На самом деле, все описания типов тут можно смело удалить, компилятор неплохо справляется с их выводом и сгенерит ту же самую программу без подсказок. Типы тут чисто для нас. Натравив конпелятор на этот код, я получил 3 КБ JavaScript'a с логикой, и ссылку на elm-runtime.js, где лежит весь-весь рантайм (включая массу неиспользуемых модулей вроде работы с Canvas и WebSockets) непожатый - около 160 КБ. Его можно closure compiler'ом ужать до 80 в базовом режиме. В продвинутом режиме, с удалением неиспользуемого кода, у меня его не получилось использовать - что-то в результате ломалось, да и выигрыш был небольшим (несколько процентов). В итоге оставил вариант с 83 КБ слегка пожатого JS, что для такой задачи все равно очень много, но все же на 200 КБ лучше, чем раньше с Flex'ом.
Работает Elm'овское FRP дискретно и push-based. Программа превращается в направленный ациклический граф, где источники сигналов, вроде every t или событий мыши, клавиатуры, окна, полей ввода и т.д. становятся вершинами-источниками (у них только исходящае дуги), а функции, преобразующие сигналы, становятся промежуточными вершинами (с входящими и выходящими дугами), main становится вершиной-приемником. Каждый раз, когда в одном из источников происходит какое-то событие (нажатие или перемещение мыши, тик таймера, нажатие кнопок и т.п.), все вершины источники генерируют по сообщению. Только там, где реально произошло событие, это сообщение с новым значением, а во всех остальных - сообщение "ничего не изменилось, старое значение такое-то". Когда в промежуточную вершину с N входами приходит N сообщений по входящим дугам, вершина проверяет, есть ли среди них новая информация или все "без изменений". Если есть новая, то вершина-функция пересчитывает свое значение с учетом новых аргументов, а иначе просто посылает вниз "без изменений", ничего не пересчитывая. В результате все работает синхронно, дискретно, и без лишних пересчетов. Есть также возможность делать нужные части графа асинхронными, чтобы долгие вычисления не замораживали весь граф (подробности могу описать отдельно, или см. доки).
Теперь о минусах. Их пока довольно много. Язык хоть и напоминает хаскель, на самом деле очень далек от него по возможностям и синтаксису. Это слегка приукрашенная просто типизированная лямбда. Есть let, но нет where. Нет do (впрочем, для него нет и повода). Паттерн-матчинг только по алгебраическим типам, простые типы не матчатся (если я правильно путаю). Есть пара псевдо-тайпклассов "Number a" и "Comparable a", но свои тайпклассы объявлять нельзя, никаких монад вам. Компилятор очень ненадежен: часто не ловит банальнейшие ошибки (вроде неизвестного идентификатора) и молча генерит код, который потом вылетает с рантайм-ошибками в браузере. Стандартная библиотека бедновата, особенно по возможным входящим сигналам - на многие события пока нельзя реагировать (например, я бы хотел реагировать на загрузку картинки - сейчас это недоступно). Документация разрозненная и неполная. Вот thesis стоит почитать, хотя бы ради обзора более ранних чужих подходов к FRP.
slides = ["slide_main.jpg", "slide_graphs.png", "slide_menu.png", "slide_interfaces.png", "slide_matching_filters.png", "slide_filter_search.png", "slide_generated_code.png", "slide_generated_code2.png", "slide_code_templates.png", "slide_connection_info.png", "slide_event_log.png", "slide_filter_info.png", "slide_grabbed_samples.png", "slide_select_mediatype.png"] ww = 440 hh = 280 nSlides = length slides
Сперва идет список имен файлов с картинками, которые будут показываться в цикле, их размер и количество. Сценарий таков: картинки сменяют друг друга не резко, а плавным кросс-фейдом. 4 секунды показываем одну картинку, потом в течение секунды идет переход к следующей. Достигается это наличием двух картинок одна под другой, первые 4 секунды верхняя картинка абсолютно прозрачна, потом ее непрозрачность плавно увеличивается от 0 до 1. По истечении 5 секунд верхняя картинка становится нижней, а верхней становится следующая. Заведем список пар имен картинок ((1,2), (2,3), (3,4) ...).
slides2 : [(String, String)] slides2 = zip slides $ drop 1 slides ++ take 1 slides nth : Int -> [a] -> a nth n lst = if n == 0 then head lst else nth (n-1) (tail lst)
Типы выглядят как хаскельные, только для объявления типа значения используется одно двоеточие, как в ML и Idris'e, а не два, как в хаскеле. nth - вспомогательная функция получения n-ного элемента списка, в стандартную библиотеку Elm'a ее забыли включить.
Происходящее на экране в нашем случае зависит только от времени. Разобьем пятисекундный интервал на 100 тиков, и будем вычислять параметры (прозрачность верхней картинки и номер пары) по номеру тика:
opacityFromTick : Int -> Float opacityFromTick n = let t = n `mod` 100 in if t < 80 then 0.0 else toFloat (t - 80) / 20.0 numFromTick : Int -> Int numFromTick n = (n `div` 100) `mod` nSlides
Отображаемый на экране контент в Elm'е имеет тип Element, и функция построения сцены по тику выглядит так:
imgPair : Int -> Element imgPair tick = let (img1, img2) = nth (numFromTick tick) slides2 in let op = opacityFromTick tick in layers [image ww hh img1, opacity op $ image ww hh img2]
Тут функция image (из стандартной библиотеки) из размеров и имени картинки делает отображаемый элемент-картинку, функция opacity меняет непрозрачность переданного ей элемента, а layers берет список элементов и располагает их на одном месте в разных слоях, от дальнего к ближнему.
Теперь у нас есть чистая функция из номера тика в отображаемый элемент, осталось добавить динамики. Динамика в Elm'e построена на принципах FRP (Functional Reactive Programming). Вместо привычных событий и их обработчиков тут есть функтор Signal, отображающий простые чистые типы в типы значений, зависящих от времени. Логически "Signal a" можно считать функцией "Time -> a". Вместо таймера у нас будет целочисленное значение "номер тика", меняющееся во времени, потому имеющее тип Signal Int:
ticks : Signal Int ticks = count $ every (50 * Time.millisecond)
Функция every возвращает значение типа Signal Float, которое обновляется через заданный интервал времени, а count превращает произвольный сигнал в Signal Int - подсчитывая число "обновлений", т.е. возвращая как раз номер тика.
Имея изменяющееся значение типа Signal Int и чистую функцию из Int в Element, мы можем применить fmap функтора, который тут называется lift, чтобы получить значение типа Signal Element - т.е. контент, изменяющийся во времени. Это и станет итоговой программой:
main : Signal Element main = lift imgPair ticks
Вот и весь код. На самом деле, все описания типов тут можно смело удалить, компилятор неплохо справляется с их выводом и сгенерит ту же самую программу без подсказок. Типы тут чисто для нас. Натравив конпелятор на этот код, я получил 3 КБ JavaScript'a с логикой, и ссылку на elm-runtime.js, где лежит весь-весь рантайм (включая массу неиспользуемых модулей вроде работы с Canvas и WebSockets) непожатый - около 160 КБ. Его можно closure compiler'ом ужать до 80 в базовом режиме. В продвинутом режиме, с удалением неиспользуемого кода, у меня его не получилось использовать - что-то в результате ломалось, да и выигрыш был небольшим (несколько процентов). В итоге оставил вариант с 83 КБ слегка пожатого JS, что для такой задачи все равно очень много, но все же на 200 КБ лучше, чем раньше с Flex'ом.
Работает Elm'овское FRP дискретно и push-based. Программа превращается в направленный ациклический граф, где источники сигналов, вроде every t или событий мыши, клавиатуры, окна, полей ввода и т.д. становятся вершинами-источниками (у них только исходящае дуги), а функции, преобразующие сигналы, становятся промежуточными вершинами (с входящими и выходящими дугами), main становится вершиной-приемником. Каждый раз, когда в одном из источников происходит какое-то событие (нажатие или перемещение мыши, тик таймера, нажатие кнопок и т.п.), все вершины источники генерируют по сообщению. Только там, где реально произошло событие, это сообщение с новым значением, а во всех остальных - сообщение "ничего не изменилось, старое значение такое-то". Когда в промежуточную вершину с N входами приходит N сообщений по входящим дугам, вершина проверяет, есть ли среди них новая информация или все "без изменений". Если есть новая, то вершина-функция пересчитывает свое значение с учетом новых аргументов, а иначе просто посылает вниз "без изменений", ничего не пересчитывая. В результате все работает синхронно, дискретно, и без лишних пересчетов. Есть также возможность делать нужные части графа асинхронными, чтобы долгие вычисления не замораживали весь граф (подробности могу описать отдельно, или см. доки).
Теперь о минусах. Их пока довольно много. Язык хоть и напоминает хаскель, на самом деле очень далек от него по возможностям и синтаксису. Это слегка приукрашенная просто типизированная лямбда. Есть let, но нет where. Нет do (впрочем, для него нет и повода). Паттерн-матчинг только по алгебраическим типам, простые типы не матчатся (если я правильно путаю). Есть пара псевдо-тайпклассов "Number a" и "Comparable a", но свои тайпклассы объявлять нельзя, никаких монад вам. Компилятор очень ненадежен: часто не ловит банальнейшие ошибки (вроде неизвестного идентификатора) и молча генерит код, который потом вылетает с рантайм-ошибками в браузере. Стандартная библиотека бедновата, особенно по возможным входящим сигналам - на многие события пока нельзя реагировать (например, я бы хотел реагировать на загрузку картинки - сейчас это недоступно). Документация разрозненная и неполная. Вот thesis стоит почитать, хотя бы ради обзора более ранних чужих подходов к FRP.
no subject
Date: 2013-07-20 10:02 pm (UTC)no subject
Date: 2013-07-21 03:52 am (UTC)Немного нехорошо, что как бы ты не старался проектировать очень типизированные генераторы джавоскрипта, в конечном счёте, столкнёшься с вариантом "Greenspun's tenth rule".
То есть, в конечном счёте, разумно делать лямбдо-язычог унутря агодочки. Благо, что это несложно. Стандартная техника — HOAS/PHOAS.
no subject
Date: 2013-07-21 08:52 am (UTC)2) компилятор состоит из парсера, тайпчекера и собственно компилятора. Этот "собственно компилятор" и представляет основной объем работ, который сохраняется и при эмбеддинге. Т.е.эмбеддинг оправдан только при маленьком EDSL с простой кодогенерацией.
3) Лямбда-язычки хорошо транслируются только в другие лямбда-язычки. Если едсл не вариант лямбда-исчисления - то эмбеддинг работает хуже.
no subject
Date: 2013-07-21 09:10 am (UTC)Синтаксис произвольный не сделать, но сделать в этом направлении можно довольно много.
2. Ну всё равно, проще получается. И главное, сохраняются все плюсы агдочки. IDE там агдочковое ;-)
3. А вот это не совсем правда. Сложные лямбды общего назначения, конечно, трудно транслировать куда-то ещё.
Но если создавать язык со специальной целью прозрачности трансляции в конкретное (например, JS), то не так всё и плохо.
_Упрощённый_ пример — соглашение передавать несколько аргументов в тупле, чтобы в джавоскрипто транслировалось более красиво с точки зрения джавоскрипта.
no subject
Date: 2013-07-21 10:48 am (UTC)Писать программу на
фортранеАгде на любом языке программирования.Именно с джавоскриптом я немного мучился. Таки можно сделать простенькую лямбду, чтобы тупо и предсказуемо транслировалась в нормальный JS.
Там сложности возникли с другим — без всей остальной инфраструктуры, просто трансляция джавоскрипта почти бесполезна.
Видимо, я ещё вернусь к этому вопросу, продумаю таки, как правильно выразить и какую часть инфраструктуры, и тогда будет иметь смысл делать что-то для практики.
no subject
Date: 2013-07-22 05:20 pm (UTC)Т.е. агдочка -> HN3 -> HN2 -> HN1 -> HN0 -> anything reasonable
no subject
Date: 2013-07-22 08:01 pm (UTC)Можно накидать в двух словах про каждый слой ?
no subject
Date: 2013-07-22 08:38 pm (UTC)Аналогично все синтаксические сахара должны быть к тому моменту рассахарены, вплоть до паттерн-матчинга замененного на ифы и функциональных средств передачи управления замененных на императивные.
В последнее время склоняюсь, что там может не быть ФВП и полиморфизма. Это может быть полезно для убогих таргетов вроде сей.
В той версии HN0, которая на гитхабе, есть ФВП и полиморфизм, но нет частичного применения. Хочу и их убрать.
Сам язычок - let, application, константы и переменные. Даже лямбд нету, вместо этого есть definition, который всегда имеет имя и не первоклассный (не может быть посреди выражения). Типа let foo x = bar in foo baz может быть, а (\x -> bar) baz - нет. Это опять же для убогих таргетов, чтобы не было идиотизма ввиде исходников, засранных нечеловекочитаемыми сгенерированными идентификаторами вроде __lambda195
no subject
Date: 2013-07-22 08:44 pm (UTC)В каких слоях должны быть:
- GADT где должен быть
- Вывод типов
- Функторы ML или Тайпклассы
и другие гриновские части.
no subject
Date: 2013-07-22 10:44 pm (UTC)Фактически я пока не определился.
> - GADT где должен быть
Всё зависит от имплементации - от того, нужно ли для представления GADT специальная низкоуровневая поддержка в кодогенераторе, или он десахаризуется в другие примитивы. Если десахаризуется - то в HN1 или выше. Если нет - то частично может быть в HN0.
> - Вывод типов
Опять же от имплементации зависит. Для генерации в джаваскрипт HN0 может быть бестиповым. В случае Си же это не прокатит. Соответственно, HN0 должен быть типизированным.
Можно выводить типы в HN1, а при переходе к HN0 стирать лишнее. А можно сделать системы типов в HN0 и HN1 независимыми: HN0 будет выводить сишные типы и эффекты, а в HN1 может быть обычной F<.
> - Функторы ML или Тайпклассы
Опять же зависит. Если вы тайпклассы в интерфейсы С++ транслировать хотите, то в HN0 они должны быть. А если вы делаете через явные словари, которые структы/произведения функций - то в HN0 они не нужны.
Я выбрал в качестве таргета Си без крестиков как наиболее трудный, буду пытаться проектировать HN0 с учетом максимально простой генерации сей.
> гриновские
Тонко!
no subject
Date: 2013-07-24 12:45 pm (UTC)GRIN в профиль. ;)
no subject
Date: 2013-07-25 06:30 am (UTC)Строго говоря, не GRIN, а UHC. HN0 - это Silly в их терминологии, и я вижу смысл делать Silly многоуровневым, а "наверх" подниматься, только когда написанная вручную программа на Silly будет транслироваться качественно.
Стратегическая разница, помимо нацеленности на сохранение идентификаторов, в том, что я планирую наращивать высокоуровневость, сохраняя качство, когда традиционный подход - наращивать качество, сохраняя высокоуровневость. Тот же UHC идет не от выходного кода, а от Хаскеля98 (в обе стороны - наверх и вниз).
no subject
Date: 2013-07-23 01:40 am (UTC)> который можно оттранслировать невероятно высококачественно
Я просто предлагаю делать его внутри агдочки.
На всякий случай, уточню — HOAS вовсе не означает лямбду.
Лямбда чисто тактически часто будет полезна.
no subject
Date: 2013-07-23 08:51 am (UTC)> На всякий случай, уточню — HOAS вовсе не означает лямбду.
Хорошо бы уточнить весь предлагаемый процесс.
Тут надо сравнивать HOAS и UUAG c HOOPL как implementation vehicles.
HOAS дает из хост-языка только бесплатную типизацию и работу с биндингами.
Как я понимаю, делаем язычок из функций, возвращающих термы HOAS, и на нем пишем программы прямо в агдочке. "Оператору пробел" в нашем язычке соответствует аппликация в HOAS, так же реюзаем непараметризованныем определениям соответствуют определения в агдочке, а параметризованным - лямбды.
С эмбеддингом у нас получается бесплатно получить типизированное AST, синтаксические сахара и средства метапрограммирования.
Но с момента получения декорированного типами AST эмбеддинг перестает приносить преимущества: всё делать становится или так же трудно, или труднее (observable sharing, например), чем в случае без эмбеддинга.
no subject
Date: 2013-07-23 09:47 am (UTC)Поначалу, я вообще хотел по-старинке, как это часто делают в хаскеле, типизированно управлять генерацией джавоскрипта.
Потом решил, что лучше будет описать примитивно типизированный язычок. Который можно будет подстраивать для целей хорошо предсказуемого контролируемого генерирования джавоскрипта.
И постепенно, наворачивать этот язычок, где потребуется.
А чего язычок принципиально не может (например, просто типизированная лямбда не может полиморфизма), то уже генерировать агдочкой.
Главное, чтобы достаточно большие куски можно было на примитивном язычке писать.
> "Оператору пробел" в нашем язычке
> соответствует аппликация в HOAS
Там всё просто и тупо.
Достаточно рассмотреть какой-нибудь пример. Вроде, полно примеров просто типизированной лямбды.
Нюансы возникают с "переменными".
Именно из-за чего я и пришёл к необходимости чего-то, похожего на HOAS/PHOAS.
> Но с момента получения декорированного типами AST
> эмбеддинг перестает приносить преимущества
Я предположил достаточно простые языки (или простые подмножества сложных языков).
В случаях, когда будет адекватно делать не сложнее простого полиморфизма (а то и просто-типа-лямбды), остальное можно доделывать агдочковым (!) метапрограммированием, и будет вполне замечательно.
Всё-таки, агдочковое метапрограммирование, благодаря своим типам, качественно отличается от какого-нибудь лиспового. Можно и подоказывать что-нибудь ;-)
Честно говоря, я не очень-очень в такой схеме уверен.
Например, представляю реализацию какого-нибудь варианта FRP и сходу получаю почти в чистом виде генерацию джавоскриптового кода.
И зачем тогда лишний промежуточный этап в виде типизированного язычка? Нууу, чтобы бОльшую часть писать на нём и только некоторое допиливать метапрограммированием.
Где золотая середина — не знаю. Но больше склоняюсь в сторону примитивного базового язычка. Изначально-то, вообще хотел в чистом виде генерировать подмножество.
Возможно, что вообще будет лучше пилить свой транслятор джавоскрипта, только более заточенный под выдачу человекочитаемого кода. Я смотрел, устроен ихний сейчашный транслятор несложно.
Вот как быть со всем остальным (CSS и DOM), пока не полностью представляю. А это важно.
Определюсь с основной моделью про DOM и взаимодействие с миром, буду думать дальше.
Если хоть идентификаторы благодаря примитивной типизации просто так не перепутаешь, уже хорошо ;-) Но совсем необязательно делать на манер типизированного jQuery UI, конечно.
no subject
Date: 2013-07-21 08:57 am (UTC)no subject
Date: 2013-07-21 09:04 am (UTC)В агдочке тоже какбе и есть, но пользоваться невозможно.
Я про описание языка внутри агдочки.
no subject
Date: 2013-07-21 09:09 am (UTC)no subject
Date: 2013-07-21 08:34 am (UTC)А если slides2 сделать бесконечным
slides2 = zip s (tail s) where s = cycle slides
и соот-но убрать nth и поменять numFromTick, всё будет ок?
Протупил, это ж Elm, он умеет бесконечности?
no subject
Date: 2013-07-21 08:53 am (UTC)А если бы и умел, не думаю, что тут получилось бы проще. Пришлось бы изобретать схему для получения Signal (String, String).
no subject
Date: 2013-07-21 08:46 am (UTC)https://gist.github.com/anonymous/6047953
no subject
Date: 2013-07-21 11:12 am (UTC)no subject
Date: 2013-07-21 03:06 pm (UTC)