Где применять Поведение (и другие виды) в FRP - PullRequest
17 голосов
/ 21 декабря 2011

Я работаю над программой, использующей реактивный банан , и мне интересно, как структурировать мои типы с помощью базовых строительных блоков FRP.

Например, вот упрощенный пример из моей настоящей программы: скажем, моя система состоит в основном из виджетов - в моей программе фрагменты текста, которые меняются со временем.

Я мог бы иметь

newtype Widget = Widget { widgetText :: Behavior String }

но я мог бы также иметь

newtype Widget = Widget { widgetText :: String }

и используйте Behavior Widget, когда я хочу поговорить об изменяющемся во времени поведении. Это, кажется, делает вещи «проще» и означает, что я могу использовать операции Behavior более напрямую, вместо того, чтобы распаковывать и перепаковывать виджеты, чтобы сделать это.

С другой стороны, первый, кажется, избегает дублирования в коде, который фактически определяет виджеты, поскольку почти все виджеты меняются со временем, и я обнаружил, что определяю даже те немногие, которые не имеют Behavior, так как это позволяет мне комбинировать их с другими более последовательно.

В качестве другого примера, для обоих представлений имеет смысл иметь экземпляр Monoid (и я хочу, чтобы он был в моей программе), но реализация для последнего кажется более естественной (поскольку это просто тривиальное снятие список моноидный к новому типу).

(Моя настоящая программа использует Discrete вместо Behavior, но я не думаю, что это актуально.)

Аналогично, я должен использовать Behavior (Coord,Coord) или (Behavior Coord, Behavior Coord) для представления 2D-точки? В этом случае первое кажется очевидным выбором; но когда это запись из пяти элементов, представляющая что-то вроде сущности в игре, выбор кажется менее очевидным.

По сути, все эти проблемы сводятся к:

При использовании FRP, к какому слою следует применять тип Behavior?

(Тот же вопрос относится и к Event, хотя и в меньшей степени.)

Ответы [ 2 ]

6 голосов
/ 22 декабря 2011

Правила, которые я использую при разработке приложений FRP:

  1. Как можно больше изолируйте «вещь, которая меняется».
  2. Сгруппируйте «вещи, которые изменяются одновременно» в один Behavior / Event.

Причина (1) состоит в том, что становится проще создавать и составлять абстрактные операции, если используемые вами типы данных настолько просты, насколько это возможно.

Причина этого заключается в том, что такие экземпляры, как Monoid, можно повторно использовать для необработанных типов, как вы описали.

Обратите внимание, что вы можете использовать Объективы , чтобы легко модифицировать "содержимое" типа данных, как если бы они были необработанными значениями, так что дополнительное "обертывание / развертывание", в основном, не является проблемой. (См. это недавнее руководство для ознакомления с этой конкретной реализацией Lens; есть others )

Причина (2) в том, что он просто устраняет ненужные накладные расходы. Если две вещи изменяются одновременно, они «имеют одинаковое поведение», поэтому их следует смоделировать как таковые.

Ergo / tl; dr : Вы должны использовать newtype Widget = Widget { widgetText :: Behavior String } из-за (1), и вы должны использовать Behavior (Coord, Coord) из-за (2) (поскольку обе координаты обычно изменяются одновременно).

5 голосов
/ 24 декабря 2011

Я согласен с советом dflemstr до

  1. Как можно больше изолируйте «вещь, которая меняется».
  2. Сгруппируйте «вещи, которые изменяются одновременно» в один Behavior/Event.

и хотел бы предложить дополнительные причины для этих правил.

Вопрос сводится к следующему: вы хотите представить пару (кортеж) значений, которые меняются во времени, и вопрос заключается в том, следует ли использовать

а. (Behavior x, Behavior y) - пара поведений

б. Behavior (x,y) - поведение пар

Причины для предпочтения одного над другим

  • a более b .

    В реализации с принудительным управлением изменение поведения вызовет пересчет всех поведений, которые зависят от него.

    Теперь рассмотрим поведение, значение которого зависит только от первого компонента x пары. В варианте a изменение второго компонента y не пересчитает поведение. Но в варианте b поведение будет пересчитано, даже если его значение вообще не зависит от второго компонента. Другими словами, речь идет о мелкозернистых и грубозависимых зависимостях.

    Это аргумент для совета 1. Конечно, это не имеет большого значения, когда оба поведения имеют тенденцию изменяться одновременно, что дает совет 2.

    Конечно, библиотека должна предлагать способ детализированных зависимостей даже для варианта b . Начиная с версии реактивного банана 0.4.3, это невозможно, но пока не беспокойтесь об этом, моя принудительная реализация в будущих версиях станет зрелой.

  • b более .

    Учитывая, что реактивная банановая версия 0.4.3 не предлагает динамическое переключение событий , однако, есть определенные программы, которые вы можете написать, только если все компоненты помещены в одно поведение. Каноническим примером может быть программа, которая имеет переменную число счетчиков, то есть расширение примера TwoCounter.hs . Вы должны представить его как изменяющийся во времени список значений

    counters :: Behavior [Int]
    

    потому что пока нет способа отслеживать динамический набор поведений. Тем не менее, следующая версия реактивного банана будет включать динамическое переключение событий.

    Кроме того, вы всегда можете без проблем конвертировать из варианта a в вариант b

    uncurry (liftA2 (,)) :: (Behavior a, Behavior b) -> Behavior (a,b)
    
...