11 нояб. 2011 г.

Введение в функциональное реактивное программирование с помощью reactive-banana

Функциональное реактивное программирование (ФРП) - это метод моделирования реактивного (т.е. зависящего от времени и внешних воздействий) поведения в чистых функциональных языках. ФРП позволяет моделировать системы, реагирующие на изменяющиеся входные воздействия, в простом декларативном стиле[1].

Цель данного поста показать, как можно писать GUI на Haskell в декларативном стиле. С помощью библиотеки reactive-banana мы напишем простой счётчик, который будет управляться двумя кнопками.


Теория


Основными концепциями ФРП являются поведения (behaviors) и события (events).

Поведение - это меняющееся со временем значение. Его можно представить как:

type Behavior a = Time -> a

Событие - это поток происшествий в порядке их появления. Событие можно представить в виде бесконечного списка значений привязанных ко времени их появления:

type Event a = [(Time, a)]

Для написания программы необходимо:

  1. получить входные поведения и события;
  2. преобразовать входные поведения и события к нужным выходным, т.е.описать логику работы;
  3. получить результаты от выходных событий.

Функции модуля 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

Всё, мы написали простой счётчик в декларативном стиле.

Ссылки
  1. Edward Amsden, A Survey of Functional Reactive Programming, 2011.
  2. Heinrich Apfelmus, Reactive-banana API reference, http://hackage.haskell.org/package/reactive-banana
  3. Исходный код Counter.lhs и PDF статьи.

1 комментарий:

  1. Спасибо, особенно за расписывание результатов работы кода.

    Вопрос не совсем по статье, а скорее по ФРП. Как правильно управлять видимостью элементов в данной парадигме?

    Например, я собираюсь реализовать элемент "спойлер" (експандер) или "tabcontrol". У таких элементов явно видно состояние, и поэтому непонятно как правильно спроектировать модель виджета без ввода состояния. Также вопрос в том, правильно ли скрывать элементы с виду в ФРП (ни один найденый в инете пример не демонстрировал такого поведения).

    С Haskell на Вы, с ФП знаком только на уровне понимания существующего кода.

    ОтветитьУдалить