Подтвердить что ты не робот

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

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

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

Я мог бы

newtype Widget = Widget { widgetText :: Behavior String }

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

newtype Widget = Widget { widgetText :: String }

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

  • Изолируйте "вещь, которая изменяется" как можно больше.
  • Группируйте "вещи, которые меняются одновременно" в один Behavior/Event.

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

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

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

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

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

  • a над b.

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

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

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

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

  • b над a.

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

    counters :: Behavior [Int]
    

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

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

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

Ответ 2

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

  • Изолируйте "вещь, которая изменяется" как можно больше.
  • Группируйте "вещи, которые меняются одновременно" в один Behavior/Event.

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

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

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

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

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