Где применять Behavior (и другие типы) в FRP

Я работаю над программой, использующей reactive-banana, и мне интересно, как структурируйте мои типы с помощью основных строительных блоков 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, хотя и в меньшей степени.)


person ehird    schedule 21.12.2011    source источник


Ответы (2)


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

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

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

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

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

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

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

  • а над б.

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

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

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

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

  • b вместо a.

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

    counters :: Behavior [Int]
    

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

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

    uncurry (liftA2 (,)) :: (Behavior a, Behavior b) -> Behavior (a,b)
    
person Heinrich Apfelmus    schedule 24.12.2011
comment
Что ж, в случае с Widget наличие только одного поля не было упрощением, это моя реальная ситуация, поэтому никакие кортежи не задействованы :) Тем не менее, спасибо за помощь — это должно быть очень полезно в будущем! Сейчас я поставлю Behavior внутри нового типа. Хотел бы я принять оба ответа :) - person ehird; 24.12.2011

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

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

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

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

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

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

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

person dflemstr    schedule 21.12.2011
comment
Я не думаю, что линзы здесь помогают — если использовать пример Monoid, то что-то вроде f = liftA2 mappend становится f a b = Widget $ mappend (widgetText a) (widgetText b). По общему признанию, подъемные комбинаторы могут облегчить эту боль. Однако я не уверен, что вы пытаетесь сказать, ссылаясь на мой пример Monoid — он должен был быть аргументом в пользу формы String, а не формы Behavior String. - person ehird; 22.12.2011
comment
Тем не менее, ваши правила звучат очень хорошо, и мне придется подумать об этом еще немного. Большое спасибо за публикацию этого! Я пока не приму этот ответ, так как хотел бы услышать другие точки зрения и точки зрения, и поскольку это довольно тонкий вопрос. - person ehird; 22.12.2011
comment
В вашем примере Widget он содержит только widgetText, что делает необработанный подъем тривиальным. Если бы у вас было больше значений в Widget, необработанное поднятие становится намного сложнее, чем поднятие Behavior через линзу и выполнение операций над ним таким образом. - person dflemstr; 22.12.2011
comment
Ах, вы имеете в виду что-то вроде liftL2 :: Lens a b -> (b -> b -> b) -> a -> a -> a? (Хм, я полагаю, вы могли бы превратить этот паттерн подъема в аппликативный функтор.) - person ehird; 22.12.2011
comment
Но у этого есть очевидная проблема: в какой аргумент вы возвращаете полученное значение? Я думаю, что использование объективов с несколькими значениями, как это, является неправильным использованием. - person ehird; 22.12.2011
comment
Конечно, вы не можете создавать экземпляры Monoid для Widget с несколькими полями, которые заботятся только о widgetText. Когда в Widget будет больше членов, вы будете выполнять над ним разные операции. Возможно, я выразился неясно: изоляция данных приводит к повторному использованию таких экземпляров, как Monoid. Если эта изоляция также приводит к утомительной развертке/перепаковке, вы можете использовать линзы, чтобы сделать развертку/перепаковку менее утомительной. Это два отдельных пункта; Я никогда не говорил, что использование линз с Monoid может быть хорошей идеей. - person dflemstr; 22.12.2011