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

FRP - потоки событий и сигналы - что теряется при использовании только сигналов?

В недавних реализациях классической FRP, например реактивной бананы, есть потоки событий и сигналы, которые являются ступенчатыми функциями (реактивные бананы называет их поведением, но тем не менее являются ступенчатыми функциями). Я заметил, что Elm использует только сигналы и не различает сигналы и потоки событий. Кроме того, реактивно-банановый режим позволяет перейти от потоков событий к сигналам (отредактирован: и это может быть способ действовать по поведению с использованием "реанимации", хотя это не считается хорошей практикой), что означает, что теоретически мы могли бы применять весь поток событий комбинаторы по сигналам/поведению, сначала преобразуя сигнал в поток событий, применяя и затем снова преобразуя. Итак, учитывая, что в целом проще использовать и изучать только одну абстракцию, в чем преимущество разделения сигналов и потоков событий? Что-то потеряно в использовании только сигналов и преобразование всех комбинаторов потока событий для работы с сигналами?

edit: Обсуждение было очень интересным. Главные выводы, которые я сам из себя сделал, это то, что поведенческие/события-источники необходимы как для взаимно-рекурсивных определений (обратной связи), так и для того, чтобы выход зависел от двух входов (поведения и источника события), но вызывал только действие, когда один из них изменяется (< @ > ).

4b9b3361

Ответ 1

(Уточнение: в реактивном банане невозможно преобразовать a Behavior обратно в Event. Функция stepper является односторонним билетом. Существует функция changes, но ее type указывает, что он "нечистый", и он содержит предупреждение о том, что он не сохраняет семантику.)

Я считаю, что наличие двух разделяющих понятий делает API более элегантным. Другими словами, это сводится к вопросу удобства использования API. Я думаю, что эти две концепции ведут себя совершенно по-другому, потому что все происходит лучше, если у вас есть два разных типа.

Например, прямой продукт для каждого типа отличается. Пара поведения эквивалентна поведению пар

(Behavior a, Behavior b) ~ Behavior (a,b)

тогда как пара событий эквивалентна событию прямой суммы:

(Event    a, Event    b) ~ Event (EitherOrBoth a b)

Если вы объедините оба типа в один, то ни одна из этих эквивалентов больше не будет храниться.

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

input <- newInput (bText :: Behavior String)

Теперь ключевой момент заключается в том, что отображаемый текст не зависит от того, как часто может быть обновлено поведение bText (к тому же или другому значению), только по самому фактическому значению. Об этом гораздо легче рассуждать, чем в другом случае, когда вам нужно подумать о том, что происходит, когда два последовательных события имеют одинаковое значение. Перерисовываете ли вы текст, когда пользователь его редактирует?

(Конечно, чтобы на самом деле рисовать текст, библиотека должна взаимодействовать с инфраструктурой графического интерфейса пользователя и отслеживать изменения в Поведении. Это то, что для комбинатора changes. Однако это может быть рассматривается как оптимизация и недоступна из "внутри FRP".)

Другой основной причиной разделения является рекурсия. Большинство событий, которые рекурсивно зависят от самих себя, не определены. Однако рекурсия всегда разрешена, если у вас есть взаимная рекурсия между событием и поведением

e = ((+) <$> b) <@> einput
b = stepper 0 e

Нет необходимости вводить задержки вручную, он просто работает из коробки.

Ответ 2

Что-то критически важное для меня потеряно, а именно сущность поведения, которая (возможно, непрерывная) вариация в течение непрерывного времени. Точная, простая, полезная семантика (независимо от конкретной реализации или исполнения) также часто теряется. Отметьте мой ответ на "Спецификация для языка функционального реактивного программирования" и следуйте ссылкам там.

Независимо от времени или в пространстве, преждевременная дискретизация препятствует сглаживанию и усложняет семантику. Рассмотрим векторную графику (и другие пространственно-непрерывные модели, такие как Pan). Как и преждевременная финализация структур данных, как описано в Почему вопросы функционального программирования.

Ответ 3

Я не думаю, что есть какая-либо польза для использования абстракции сигналов/поведения над сигналами стиля вязов. Как вы заметили, возможно создать API только для сигналов поверх API сигнала/поведения (совсем не готовый к использованию, но см. https://github.com/JohnLato/impulse/blob/dyn2/src/Reactive/Impulse/Syntax2.hs для примера). Я уверен, что также возможно написать API сигнала/поведения поверх API стиля вяза. Это сделало бы два API функционально эквивалентными.

Эффективность WRT, с API только для сигналов, система должна иметь механизм, в котором только сигналы с обновленными значениями будут вызывать пересчеты (например, если вы не перемещаете мышь, сеть FRP не будет переучитывать указатель координаты и перерисовка экрана). Если это будет сделано, я не думаю, что есть потеря эффективности по сравнению с подходом к сигналам и потокам. Я уверен, что Элм работает таким образом.

Я не думаю, что проблема непрерывного поведения здесь имеет значение (или действительно вообще). То, что люди подразумевают, говоря, что поведение постоянно со временем, состоит в том, что они определяются во все времена (т.е. Являются функциями над непрерывной областью); само поведение не является непрерывной функцией. Но на самом деле у нас нет способа опробовать поведение в любое время; они могут быть отобраны только по времени, соответствующим событиям, поэтому мы не можем использовать полную мощность этого определения!

Семантически, начиная с этих определений:

Event    == for some t ∈ T: [(t,a)]
Behavior == ∀ t ∈ T: t -> b

поскольку поведение может быть отображено только в моменты, когда события определены, мы можем создать новый домен TX, где TX - это набор всех времен t, в котором определены события. Теперь мы можем ослабить определение поведения

Behavior == ∀ t ∈ TX: t -> b

без потери мощности (т.е. это эквивалентно первоначальному определению в пределах нашей системы frp). Теперь мы можем перечислить все времена в TX, чтобы преобразовать это в

Behavior == ∀ t ∈ TX: [(t,b)]

который идентичен исходному определению Event, за исключением домена и количественной оценки. Теперь мы можем изменить область Event на TX (по определению TX) и количественную оценку Behavior (от forall до для некоторого) и получим

Event    == for some t ∈ TX: [(t,a)]
Behavior == for some t ∈ TX: [(t,b)]

а теперь Event и Behavior являются семантически идентичными, поэтому они, очевидно, могут быть представлены с использованием той же структуры в системе FRP. На этом этапе мы теряем немного информации; если мы не будем различать Event и Behavior, мы не знаем, что a Behavior определяется каждый раз t, но на практике я не думаю, что это действительно имеет значение. В каком влюсе IIRC требуется как Event, так и Behavior иметь значения во все времена и просто использовать предыдущее значение для Event, если оно не изменилось (т.е. Изменить квантификацию Event на forall вместо изменения квантования Behavior). Это означает, что вы можете рассматривать все как сигнал, и все это Just Works; он просто реализован так, чтобы сигнальный домен был точно подмножеством времени, которое система фактически использует.

Я думаю, что эта идея была представлена ​​в статье (которую я не могу найти сейчас, у кого-нибудь еще есть ссылка?) о внедрении FRP в Java, возможно, из POPL'14? Работая из памяти, мой план не столь строгий, как оригинальное доказательство.

Нет ничего, что помешало бы вам создать более определенный Behavior, например. pure someFunction, это просто означает, что в системе FRP вы не можете использовать эту дополнительную определенность, поэтому ничего не теряется в более ограниченной реализации.

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

Короче говоря, я не думаю, что что-то теряется, используя только сигналы.

Ответ 4

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

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