什么是(功能)反应式编程?

时间:2009-06-22 16:41:19

标签: functional-programming terminology reactive-programming frp

我在reactive programming上读过维基百科的文章。我还阅读了functional reactive programming上的小文章。描述非常抽象。

  1. 功能反应式编程(FRP)在实践中意味着什么?
  2. 反应式编程(与非反应式编程相反?)是由什么组成的?
  3. 我的背景是强制性/ OO语言,因此我们将非常感谢与此范例相关的解释。

18 个答案:

答案 0 :(得分:932)

如果你想了解一下FRP,你可以从1998年的旧版Fran tutorial开始,它有动画插图。对于论文,请从Functional Reactive Animation开始,然后跟进我主页上的出版物链接和FRP上的Haskell wiki链接上的链接。

就个人而言,我想在讨论如何实施之前考虑FRP 的含义。 (没有规范的代码是一个没有问题的答案,因此“甚至没有错”。) 所以我没有在表达/实现术语中描述FRP,正如Thomas K在另一个答案中所做的那样(图形,节点,边缘,触发,执行等)。 有许多可能的实现样式,但没有实现说明什么是FRP

我确实与Laurence G的简单描述产生共鸣,即FRP是关于“随时间变化代表价值的数据类型”。 传统的命令式编程仅通过状态和突变间接捕获这些动态值。 完整的历史(过去,现在,将来)没有一流的代表。 此外,由于命令式范式在时间上是离散的,因此只能(间接地)捕获离散演化的值。 相比之下,FRP直接捕获这些不断发展的值,并且持续不断发展的价值没有任何困难。

FRP也很不寻常,因为它并没有与理论上的和谐相提并论。困扰命令性并发的实用大鼠巢。 从语义上讲,FRP的并发性是细粒度确定连续。 (我说的是意义,而不是实现。实现可能会也可能不会涉及并发或并行。) 语义确定性对于推理非常重要,无论是严谨的还是非正式的。 虽然并发性为命令式编程增加了极大的复杂性(由于不确定的交错),但它在FRP中毫不费力。

那么,什么是FRP? 你可以自己发明它。 从这些想法开始:

  • 动态/演变值(即“随时间变化的值”)本身就是第一类值。您可以定义它们并将它们组合,将它们传递给&没有功能。我称这些事为“行为”。

  • 行为是由一些基元构建的,例如常量(静态)行为和时间(如时钟),然后是顺序和并行组合。 n 行为通过应用n-ary函数(在静态值上),“逐点”,即随着时间的推移而连续组合。

  • 为了解释离散现象,有另一种类型(系列)的“事件”,每个事件都有一个流(有限或无限)出现。每次出现都有相关的时间和价值。

  • 要想出可以构建所有行为和事件的构图词汇,请参考一些示例。将解构分解为更通用/更简单的部分。

  • 因此,您知道自己处于坚实的基础,使用指称语义技术为整个模型提供一个构成基础,这意味着(a)每种类型都有相应的简单&精确数学类型的“含义”,和(b)每个原语和操作符都有一个简单的&作为成分含义的函数的精确含义。 永远不会将实施注意事项混合到您的探索过程中。如果此描述对您来说是胡言乱语,请参阅(a) Denotational design with type class morphisms ,(b) Push-pull functional reactive programming (忽略实现位)和(c) )Denotational Semantics Haskell wikibooks page。请注意,指称语义有两个部分,分别来自两位创始人Christopher Strachey和Dana Scott:更容易和更容易。更有用的Strachey部分,更难和更少用(对于软件设计)Scott部分。

如果你坚持这些原则,我希望你能从FRP的精神中获得更多或更少的东西。

我从哪里获得这些原则?在软件设计中,我总是问同样的问题:“它是什么意思?”。 表达语义给了我一个精确的框架来解决这个问题,并且符合我的美学(不同于操作或公理语义,这两者都让我不满意)。 所以我问自己什么是行为? 我很快意识到,命令式计算的暂时离散性质是对机器的特定风格的适应性,而不是对行为本身的自然描述。 我能想到的最简单的行为描述就是“(连续)时间的功能”,这就是我的模型。 令人欣喜的是,这个模型轻松而优雅地处理连续,确定的并发。

正确有效地实施这个模型真是一个挑战,但这是另一个故事。

答案 1 :(得分:740)

在纯函数式编程中,没有副作用。对于许多类型的软件(例如,任何具有用户交互的东西),在某种程度上都需要副作用。

在保留功能风格的同时获得类似行为的副作用的一种方法是使用功能性反应式编程。这是函数式编程和反应式编程的组合。 (你链接的维基百科文章是关于后者的。)

反应式编程背后的基本思想是某些数据类型代表“随时间变化”的值。涉及这些随时间变化的值的计算本身会有随时间变化的值。

例如,您可以将鼠标坐标表示为一对整数时间值。假设我们有类似的东西(这是伪代码):

x = <mouse-x>;
y = <mouse-y>;

在任何时候,x和y都会有鼠标的坐标。与非反应式编程不同,我们只需要进行一次此分配,x和y变量将自动保持“最新”。这就是为什么反应式编程和函数式编程能够很好地协同工作的原因:反应式编程消除了变异变量的需要,同时仍然允许你通过变量突变完成许多工作。

如果我们基于此进行一些计算,结果值也将是随时间变化的值。例如:

minX = x - 16;
minY = y - 16;
maxX = x + 16;
maxY = y + 16;

在此示例中,minX将始终比鼠标指针的x坐标小16。使用反应感知库,您可以说:

rectangle(minX, minY, maxX, maxY)

鼠标指针周围会绘制一个32x32的盒子,无论它移动到哪里都会跟踪它。

这是一个非常好的paper on functional reactive programming

答案 2 :(得分:144)

想象一下你的程序是一个电子表格并且所有变量都是单元格的简单方法是获得关于它的第一个直觉。如果电子表格中的任何单元格发生更改,则引用该单元格的任何单元格也会发生更改。它和FRP一样。现在想象一些单元格自行改变(或者更确切地说,取自外部世界):在GUI情况下,鼠标的位置将是一个很好的例子。

这必然会错过很多。当你实际使用FRP系统时,这个比喻会很快崩溃。例如,通常也尝试对离散事件进行建模(例如,点击鼠标)。我只是把它放在这里,让你知道它是什么样的。

答案 3 :(得分:131)

对我来说,这是符号=的两个不同含义:

  1. 在数学x = sin(t)中,x的{​​{1}} 名称 <{1}}。所以写sin(t)x + y是一回事。在这方面,功能性反应式编程就像数学一样:如果你编写sin(t) + y,则使用x + y的值来计算它。
  2. 在类C编程语言(命令式语言)中,t是一项任务:它表示x = sin(t)存储 x值。转让的时间。

答案 4 :(得分:71)

好的,从背景知识和阅读您指向的维基百科页面看来,似乎反应性编程类似于数据流计算,但具有特定的外部“刺激”触发一组节点以触发并执行其计算。

这非常适合UI设计,例如,触摸用户界面控件(例如,音乐播放应用程序上的音量控制)可能需要更新各种显示项目和音频输出的实际音量。当你修改音量(一个滑块,比方说)时,它对应于修改与有向图中节点相关的值。

具有来自该“音量值”节点的边缘的各种节点将自动被触发,并且任何必要的计算和更新将自然地波及整个应用程序。应用程序“响应”用户刺激。功能性反应式编程只是在函数式语言中实现,或者通常在函数式编程范式中实现。

有关“数据流计算”的更多信息,请在维基百科上搜索这两个单词或使用您最喜爱的搜索引擎。一般的想法是这样的:程序是节点的有向图,每个节点执行一些简单的计算。这些节点通过图形链接相互连接,图形链接将某些节点的输出提供给其他节点的输入。

当节点触发或执行其计算时,连接到其输出的节点将其相应的输入“触发”或“标记”。触发/标记/可用的所有输入的任何节点都会自动触发。该图可能是隐式或显式的,具体取决于如何实现反应式编程。

节点可以被视为并行触发,但通常它们是串行执行的或者具有有限的并行性(例如,可能有几个线程执行它们)。一个着名的例子是Manchester Dataflow Machine,其中(IIRC)使用标记数据架构来通过一个或多个执行单元来调度图中节点的执行。数据流计算非常适合于这样的情况,其中异步触发计算产生级联计算比尝试执行由时钟(或时钟)控制更好。

反应式编程导入了这种“级联执行”的想法,并且似乎以类似数据流的方式来考虑该程序,但条件是某些节点被连接到“外部世界”并且触发了级联执行当这些感觉状节点改变时。然后程序执行看起来像复杂的反射弧。该程序在刺激之间可能基本上是无柄的,也可能不是刺激之间的基本无柄状态。

“非反应性”编程将使用与执行流程和与外部输入的关系的非常不同的视图进行编程。这可能有点主观,因为人们很可能会说任何对外部投入有反应的事情都会对他们做出“反应”。但是看一下这个东西的精神,一个以固定间隔轮询事件队列并调度发现给函数(或线程)的任何事件的程序反应性较小(因为它只会以固定的间隔参与用户输入)。再一次,这就是这里的精神:人们可以想象将具有快速轮询间隔的轮询实现放入一个非常低级别的系统中,并以一种被动方式编程。

答案 5 :(得分:65)

在阅读了很多关于FRP的文章之后,我终于遇到了this关于FRP的启发性写作,它最终让我明白了FRP究竟是什么。

我引用了Heinrich Apfelmus(反应性香蕉的作者)。

  

功能反应式编程的本质是什么?

     

一个常见的答案是“FRP就是要描述一个系统   时变函数的术语而不是可变状态“,以及   肯定没错。这是语义观点。但在   我认为,更深刻,更令人满意的答案是由   遵循纯粹的句法标准:

     

功能性反应式编程的本质是在声明时完全指定值的动态行为。

     

例如,以计数器为例:您有两个按钮   标记为“向上”和“向下”,可用于递增或递减   柜台。当然,您首先要指定一个初始值   然后在按下按钮时更改它;像这样的东西:

counter := 0                               -- initial value
on buttonUp   = (counter := counter + 1)   -- change it later
on buttonDown = (counter := counter - 1)
     

关键是在声明时,只有初始值   指定柜台;计数器的动态行为是   隐含在程序文本的其余部分中。相比之下,功能性   反应式编程指定了当时的整个动态行为   声明,像这样:

counter :: Behavior Int
counter = accumulate ($) 0
            (fmap (+1) eventUp
             `union` fmap (subtract 1) eventDown)
     

每当你想要理解计数器的动态时,你就只有   看看它的定义。可能发生的一切都会发生   出现在右侧。这与之形成鲜明对比   后续声明可以改变的必要方法   先前声明的值的动态行为。

所以,在我的理解中,FRP程序是一组方程式: enter image description here

j是离散的:1,2,3,4 ...

f取决于t所以这包含了模拟外部刺激的可能性

程序的所有状态都封装在变量x_i

FRP图书馆负责处理进度时间,换句话说,将j带到j+1

我在this视频中更详细地解释了这些方程式。

修改

在最初答案之后大约2年,最近我得出的结论是,FRP实施还有另一个重要方面。他们需要(并且通常会)解决一个重要的实际问题:缓存失效

x_i - s的等式描述了一个依赖图。当某些x_i在时间j发生更改时,并非x_i'处的所有其他j+1值都需要更新,因此并非所有依赖项都需要重新计算,因为某些x_i'可能独立于x_i

此外,x_i - s可以逐步更新。例如,让我们考虑Scala中的地图操作f=g.map(_+1),其中fgList Ints。此处f对应x_i(t_j)g对应x_j(t_j)。现在,如果我将一个元素添加到g,那么对map中的所有元素执行g操作将会很浪费。一些FRP实现(例如reflex-frp)旨在解决此问题。此问题也称为incremental computing.

换句话说,FRP中的行为(x_i - s)可以被认为是缓存计算。如果某些x_i - s确实发生了变化,那么FRP引擎的任务就是有效地使这些缓存(f_i - s)无效并重新计算。

答案 6 :(得分:29)

Conal Elliott撰写的论文 Simply efficient functional reactivity direct PDF,233 KB)是一个相当不错的介绍。相应的库也可以使用。

该论文现已被其他论文取代, Push-pull functional reactive programming direct PDF,286 KB)。

答案 7 :(得分:29)

免责声明:我的答案是在rx.js的背景下 - 一个反应式编程&#39; Javascript库。

在函数式编程中,您不是迭代集合中的每个项目,而是将更高阶函数(HoF)应用于集合本身。因此,FRP背后的想法是,不是处理每个单独的事件,而是创建事件流(使用可观察的*实现)并将HoF应用于该事件。通过这种方式,您可以将系统可视化为将发布者与订阅者连接起来的数据管道。

使用可观察的主要优点是:
i)它从您的代码中抽象出状态,例如,如果您希望事件处理程序仅针对每个第n个事件被触发,或者在第一个事件之后停止触发。事件,或者只有在第一个&n;事件,你可以使用HoFs(过滤器,takeUntil,分别跳过)而不是设置,更新和检查计数器。
ii)它改进了代码局部性 - 如果你有5个不同的事件处理程序改变组件的状态,你可以合并它们的observable并在合并的observable上定义一个事件处理程序,有效地将5个事件处理程序组合成1.这使得它非常很容易推断整个系统中的哪些事件会影响组件,因为它们都存在于单个处理程序中。

  • Observable是Iterable的双重身份。

Iterable是一个懒惰消耗的序列 - 每当它想要使用它时,每个项都由迭代器拉动,因此枚举由消费者驱动。

一个observable是一个延迟生成的序列 - 每当项被添加到序列时,每个项都被推送给观察者,因此枚举由生产者驱动。

答案 8 :(得分:18)

老兄,这是一个非常棒的主意!为什么我在1998年没有发现这个?无论如何,这是我对Fran教程的解释。建议是最受欢迎的,我正在考虑基于此开始一个游戏引擎。

import pygame
from pygame.surface import Surface
from pygame.sprite import Sprite, Group
from pygame.locals import *
from time import time as epoch_delta
from math import sin, pi
from copy import copy

pygame.init()
screen = pygame.display.set_mode((600,400))
pygame.display.set_caption('Functional Reactive System Demo')

class Time:
    def __float__(self):
        return epoch_delta()
time = Time()

class Function:
    def __init__(self, var, func, phase = 0., scale = 1., offset = 0.):
        self.var = var
        self.func = func
        self.phase = phase
        self.scale = scale
        self.offset = offset
    def copy(self):
        return copy(self)
    def __float__(self):
        return self.func(float(self.var) + float(self.phase)) * float(self.scale) + float(self.offset)
    def __int__(self):
        return int(float(self))
    def __add__(self, n):
        result = self.copy()
        result.offset += n
        return result
    def __mul__(self, n):
        result = self.copy()
        result.scale += n
        return result
    def __inv__(self):
        result = self.copy()
        result.scale *= -1.
        return result
    def __abs__(self):
        return Function(self, abs)

def FuncTime(func, phase = 0., scale = 1., offset = 0.):
    global time
    return Function(time, func, phase, scale, offset)

def SinTime(phase = 0., scale = 1., offset = 0.):
    return FuncTime(sin, phase, scale, offset)
sin_time = SinTime()

def CosTime(phase = 0., scale = 1., offset = 0.):
    phase += pi / 2.
    return SinTime(phase, scale, offset)
cos_time = CosTime()

class Circle:
    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius
    @property
    def size(self):
        return [self.radius * 2] * 2
circle = Circle(
        x = cos_time * 200 + 250,
        y = abs(sin_time) * 200 + 50,
        radius = 50)

class CircleView(Sprite):
    def __init__(self, model, color = (255, 0, 0)):
        Sprite.__init__(self)
        self.color = color
        self.model = model
        self.image = Surface([model.radius * 2] * 2).convert_alpha()
        self.rect = self.image.get_rect()
        pygame.draw.ellipse(self.image, self.color, self.rect)
    def update(self):
        self.rect[:] = int(self.model.x), int(self.model.y), self.model.radius * 2, self.model.radius * 2
circle_view = CircleView(circle)

sprites = Group(circle_view)
running = True
while running:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False
        if event.type == KEYDOWN and event.key == K_ESCAPE:
            running = False
    screen.fill((0, 0, 0))
    sprites.update()
    sprites.draw(screen)
    pygame.display.flip()
pygame.quit()

简而言之:如果每个组件都可以像数字一样处理,那么整个系统可以像数学公式一样对待,对吧?

答案 9 :(得分:14)

Paul Hudak的书The Haskell School of Expression,不仅是对Haskell的精彩介绍,而且在FRP上花费了相当多的时间。如果您是FRP的初学者,我强烈建议您了解FRP的工作原理。

本书的新内容(2011年发布,2014年更新),The Haskell School of Music也是如此。

答案 10 :(得分:10)

根据之前的答案,似乎在数学上,我们只是按更高的顺序思考。我们不考虑具有 X 类型的值 x ,而是考虑函数 x T X ,其中 T 是时间的类型,无论是自然数,整数还是连续统。现在,当我们在编程语言中编写 y := x + 1时,我们实际上意味着等式 y t )= x t )+ 1。

答案 11 :(得分:9)

就像电子表格一样。通常基于事件驱动的框架。

与所有“范式”一样,它的新颖性值得商榷。

根据我对演员的分布式流网络的经验,它很容易成为整个节点网络中状态一致性的一般问题的牺牲品,即最终会在奇怪的循环中产生大量振荡和陷阱。

这很难避免,因为某些语义意味着参考循环或广播,并且当演员网络在一些不可预测的状态上收敛(或不收敛)时可能会非常混乱。

同样,尽管有明确定义的边缘,但可能无法达到某些状态,因为全球状态偏离了解决方案。 2 + 2可能会或可能不会变为4取决于2的2变为2,以及他们是否保持这种状态。电子表格具有同步时钟和循环检测。分布式演员通常不会。

一切都很有趣:)。

答案 12 :(得分:8)

我在关于FRP的Clojure subreddit上发现了这个不错的视频。即使你不了解Clojure,它也很容易理解。

以下是视频:http://www.youtube.com/watch?v=nket0K1RXU4

以下是视频在下半部分引用的来源:https://github.com/Cicayda/yolk-examples/blob/master/src/yolk_examples/client/autocomplete.cljs

答案 13 :(得分:7)

Andre Staltz的

This article是迄今为止我见过的最好,最清晰的解释。

文章中的一些引用:

  

反应式编程是使用异步数据流进行编程。

     

最重要的是,您将获得一个惊人的功能工具箱来组合,创建和过滤任何这些流。

这是作为本文一部分的精彩图表的一个例子:

Click event stream diagram

答案 14 :(得分:5)

它是关于随时间推移的数学数据转换(或忽略时间)。

在代码中,这意味着功能纯度和声明性编程。

状态错误在标准命令式范例中是一个巨大的问题。各种代码位可能在程序执行中的不同“时间”改变某些共享状态。这很难处理。

在FRP中,您描述(如在声明性编程中)数据如何从一种状态转换为另一种状态以及触发它的原因。这允许您忽略时间,因为您的函数只是对其输入作出反应并使用它们的当前值来创建新的输入。这意味着状态包含在转换节点的图形(或树)中,并且在功能上是纯粹的。

这大大降低了复杂性和调试时间。

想想数学中A = B + C和程序中A = B + C之间的区别。 在数学中,你描述的是一种永远不会改变的关系。在一个程序中,它说“现在”A是B + C.但是下一个命令可能是B ++,在这种情况下A不等于B + C.在数学或声明性编程中,无论你提出什么时间点,A总是等于B + C.

因此,通过消除共享状态的复杂性和随时间变化的值。你的程序更容易推理。

EventStream是一个EventStream +一些转换函数。

行为是一个EventStream +内存中的一些值。

当事件触发时,通过运行转换函数更新值。它产生的值存储在行为记忆中。

可以组织行为以产生对N个其他行为进行转换的新行为。当输入事件(行为)触发时,此组合值将重新计算。

“由于观察者是无状态的,我们经常需要其中几个模拟状态机,就像在拖动示例中一样。我们必须保存所有相关观察者可以访问的状态,例如在上面的变量路径中。”< / p>

引用自 - 弃用观察者模式 http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf

答案 15 :(得分:2)

关于反应式编程的简短明确的解释出现在Cyclejs - Reactive Programming上,它使用简单和直观的样本。

  

[module / Component / object] 是被动的意味着它是完全负责任的   通过对外部事件做出反应来管理自己的状态。

     

这种方法有什么好处?它是控制反转,   主要是因为[module / Component / object]对自己负责,使用私有方法改进对公共方法的封装。

这是一个很好的创业点,而不是知识的完整来源。从那里你可以跳到更复杂和更深的论文。

答案 16 :(得分:0)

查看Rx,.NET的Reactive Extensions。他们指出,使用IEnumerable,你基本上是在拉动&#39;从一个流。 Linq对IQueryable / IEnumerable的查询是设置操作,这些操作是“吮吸”的。一组中的结果。但是通过IObservable上的相同运算符,您可以编写“反应”的Linq查询。

例如,您可以编写类似的Linq查询 (来自MyObservableSetOfMouseMovements中的m 其中m.X <100且m.Y <100 选择新点(m.X,m.Y))。

并使用Rx扩展程序,它就是:你有一个UI代码可以对输入的鼠标移动流做出反应,只要你在100,100的方框中就会进行绘制......

答案 17 :(得分:0)

FRP是函数式编程(基于一切都是函数的编程范式)和反应式编程范式的组合(建立在一切都是流(观察者和可观察哲学)的思想的基础上)。它应该是世界上最好的。

请查看Andre Staltz关于反应式编程的帖子。