Функциональное реактивное программирование (ФРП) - это метод моделирования реактивного (т.е. зависящего от времени и внешних воздействий) поведения в чистых функциональных языках. ФРП позволяет моделировать системы, реагирующие на изменяющиеся входные воздействия, в простом декларативном стиле[1].
Цель данного поста показать, как можно писать GUI на Haskell в декларативном стиле. С помощью библиотеки reactive-banana мы напишем простой счётчик, который будет управляться двумя кнопками.
Теория
Основными концепциями ФРП являются поведения (behaviors) и события (events).
Поведение - это меняющееся со временем значение. Его можно представить как:
type Behavior a = Time -> a
Событие - это поток происшествий в порядке их появления. Событие можно представить в виде бесконечного списка значений привязанных ко времени их появления:
type Event a = [(Time, a)]
Для написания программы необходимо:
- получить входные поведения и события;
- преобразовать входные поведения и события к нужным выходным, т.е.описать логику работы;
- получить результаты от выходных событий.
Функции модуля Reactive.Banana.Implementation позволяют получать входные события и поведения от внешних источников. Например, последовательность нажатий на кнопку мыши можно представить как событие, а строку в поле ввода как значение зависящее от времени, т.е. поведение.
Функции модуля Reactive.Banana.Model позволяют описывать новые события на основе входных событий. Это описание называется графом событий. Граф событий - это своего рода описание логики работы, требуемая функциональность достигается путём комбинирования существующих событий и поведений.
Полученные путём использованием комбинаторов события выводятся с помощью функций модуля Reactive.Banana.Implementation.
Сеть событий - это граф событий вместе с входами и выходами. Для описания сети событий необходимо описать входы, выходы и граф событий в монаде NetworkDescription, а затем использовать функцию compile.
Для активации сети событий нужно использовать функцию actuate. Процесс активации заключается в регистрации входных событий и начале формирования результатов.
Практика
Начнём с импорта необходимых библиотек. Нам понадобятся:
- wxHaskell для GUI,
- reactive-banana для ФРП,
- reactive-banana-wx для связывания wxHaskell с reactive-banana.
import Control.Monad
import Graphics.UI.WX hiding (Event)
import Reactive.Banana
import Reactive.Banana.WX
Начнём писать функцию main:
main = start $ do
f <- frame [text := "Counter"]
bup <- button f [text := "Up"]
bdown <- button f [text := "Down"]
output <- staticText f []
set f [layout := margin 10 $
column 5 [widget bup, widget bdown, widget output]]
Здесь мы создали основное окно с двумя кнопками (одна - для увеличения значения счётчика, другая - для уменьшения) и одним текстовым полем (для вывода значения счетчика).
Следующий шаг - описание сети и её компиляция для получения сети событий:
network <- compile $ do
Описание сети начинаем с определения её входов:
eup <- event0 bup command
edown <- event0 bdown command
Здесь мы использовали функцию event0 для получения событий от нажатий на кнопки. Функция |event0| предоставляется модулем Reactive.Banana.WX и служит для получения событий из wxHaskell. Т.о., если мы нажимаем на кнопки "Up" и "Down" поочереди, то мы генерируем два потока происшествий, которые можно представить как:
eup = [(time1,bupCommand),(time3,bupCommand)..]
edown = [(time2,bdownCommand),(time4,bdownCommand)..]
Следующий шаг --- описание графа событий.
Сначала мы формируем новое событие на основе входного. Используем комбинатор, который заменяет все происшествия с нажатием на кнопку на функцию |(+1)|:
let
einc :: Event (Int -> Int)
einc = (+1) <$ eup
Функция (<$) :: a -> f b -> f a заменяет все входные происшествия одинаковым значением. Результат её работы можно представить так:
(+1) <$ [(time1,bupCommand),(time3,bupCommand)..] =
[(time1,(+1)),(time3,(+1))..]
Такую же операцию проводим с событием edown, но здесь используем другую функцию - (subtract 1):
edec :: Event (Int -> Int)
edec = subtract 1 <$ edown
Объединим эти два события в одно:
ecount :: Event (Int -> Int)
ecount = einc `union` edec
Функция union объединяет два потока происшествий одинакового типа:
[(time1,(+1)),(time3,(+1))] `union` [(time2,subtract 1),(time4,subtract 1)] =
[(time1,(+1)),(time2,subtract 1),(time3,(+1)),(time4,subtract 1)]
Наконец, мы используем функцию accumD для получения меняющегося значения счётчика из события:
counter :: Discrete Int
counter = accumD 0 ecount
Полученный счётчик обладает типом Discrete, который почти идентичен типу Behavior (за описанием отличий можно обратиться документации на reactive-banana).
Функция accumD аналогична функции foldl', её можно представить как:
accumD 0 [(time1,(+1)),(time2,subtract 1),(time3,(+1))..] =
stepperD 0 [(time1,1),(time2,0),(time3,1)..]
где stepperD - возвращает функцию от времени, получая начальное значение и поток новых значений:
stepperD x0 ex = \time ->
last (x0 : [x | (timeX,x) <- ex, timeX < time])
Следующий шаг - получение результатов. Сначала мы применяем функцию show для преобразования изменяющегося значения с типом Int к типу String:
counterStr :: Discrete String
counterStr = show <$> counter
Затем используем функцию sink для отображения результатов:
sink output [text :== counterStr]
sink - вспомогательная функция с типом w -> [Prop' w] -> NetworkDescription () из модуля Reactive.Banana.WX. Она получает виджет и список элементов с типом Prop', где Prop' - это атрибут wxHaskell ассоциированный с изменяющимся значением. Ассоциация выполняется с помощью оператора (:==), имеющего тип Attr w a -> Discrete a -> Prop' w.
Заклютельный шаг - активация сети событий. Сеть регистрирует входные обработчики событий и начинает формировать результаты.
actuate network
Всё, мы написали простой счётчик в декларативном стиле.
Ссылки
- Edward Amsden, A Survey of Functional Reactive Programming, 2011.
- Heinrich Apfelmus, Reactive-banana API reference, http://hackage.haskell.org/package/reactive-banana
- Исходный код Counter.lhs и PDF статьи.
Спасибо, особенно за расписывание результатов работы кода.
ОтветитьУдалитьВопрос не совсем по статье, а скорее по ФРП. Как правильно управлять видимостью элементов в данной парадигме?
Например, я собираюсь реализовать элемент "спойлер" (експандер) или "tabcontrol". У таких элементов явно видно состояние, и поэтому непонятно как правильно спроектировать модель виджета без ввода состояния. Также вопрос в том, правильно ли скрывать элементы с виду в ФРП (ни один найденый в инете пример не демонстрировал такого поведения).
С Haskell на Вы, с ФП знаком только на уровне понимания существующего кода.