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

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

"правка": обсуждение было очень интересным. Основные выводы, которые я сделал из обсуждения, заключаются в том, что поведение/источники событий необходимы как для взаимно рекурсивных определений (обратная связь), так и для того, чтобы выход зависел от двух входов (поведение и источник событий), но только вызывают действие, когда один из них изменяется ().

4 ответов


(уточнение: в реактивном банане невозможно преобразовать Behavior на Event. The stepper функция билет в один конец. Есть changes функция, но ее тип указывает на то, что она" нечиста", и она поставляется с предупреждением о том, что она не сохраняет семантику.)

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

например,прямое произведение для каждого типа разная. Пара поведения эквивалентна поведению пар

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

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

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

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

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

input <- newInput (bText :: Behavior String)

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

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

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

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

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


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

ли во времени или в пространстве, преждевременная дискретизация препятствует композиционности и усложняет семантику. Рассмотрим векторную графику (и другие пространственно непрерывные модели, такие как Пан ' s). Так же, как с преждевременной финизацией структур данных, как описано в Почему Функциональное Программирование Имеет Значение.


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

эффективность 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 to 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, но на практике я не думаю, что это действительно важно. То, что elm делает IIRC, требует обоих Events и Behaviors, чтобы иметь значения во все времена и просто использовать Предыдущее значение Event если он не изменился (т. е. изменить количественную оценку Event to forall вместо изменения количественной оценке Behavior). Это означает, что вы можете рассматривайте все как сигнал, и все это просто работает; он просто реализован так, что сигнальная область-это именно подмножество времени, которое фактически использует система.

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

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

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

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


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

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