Надысь побаловался 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-21 09:09 am (UTC)