关于术语:什么是(功能性)反应式编程?

What is (functional) reactive programming?

我读过维基百科关于反应式编程的文章。我还阅读了有关函数式反应式编程的小文章。这些描述相当抽象。

  • 功能反应式编程(FRP)在实践中是什么意思?
  • 反应式编程(与非反应式编程相反)是什么?是由什么组成的?
  • 我的背景是命令式/OO语言,因此,我们将感谢与此范例相关的解释。


    如果你想体验一下玻璃钢,你可以从1998年的旧FRAN教程开始,它有动画插图。对于论文,从功能反应动画开始,然后在我的主页上的出版物链接和haskell wiki上的FRP链接上跟进。好的。

    就我个人而言,在讨论如何实施FRP之前,我喜欢思考它的含义。(没有规范的代码是一个没有问题的答案,因此"甚至没有错"。)所以我并没有用表示/实现术语来描述FRP,就像托马斯K在另一个答案(图、节点、边、点火、执行等)中所做的那样。有许多可能的实现样式,但没有一个实现说明什么是FRP。好的。

    我确实与劳伦斯G的简单描述产生了共鸣,即FRP是关于"表示‘随时间’变化的值的数据类型"。传统的命令式编程只能通过状态和突变间接地捕获这些动态值。完整的历史(过去、现在、未来)没有一流的代表性。此外,由于命令式范式是暂时离散的,因此只能(间接)捕获离散的进化值。相比之下,玻璃钢直接捕捉到这些进化值,并且对于持续进化值没有困难。好的。

    FRP也不常见,因为它是并行的,而不违背理论和实用的老鼠窝,这困扰了命令式并发。从语义上讲,FRP的并发性是细粒度的、确定性的和连续的。(我说的是意义,不是实现。实现可能涉及或不涉及并发性或并行性。)语义确定性对于推理是非常重要的,无论是严格的还是非正式的。虽然并发性给命令式编程增加了巨大的复杂性(由于非确定性交织),但在FRP中却很容易实现。好的。

    那么,什么是玻璃钢?你可以自己发明的。从这些想法开始:好的。

    • 动态/进化值(即"随时间"变化的值)本身就是一流的值。您可以定义它们并将它们组合起来,然后将它们传入和传出函数。我把这些叫做"行为"。好的。

    • 行为是由一些基本元素组成的,比如常量(静态)行为和时间(比如时钟),然后是顺序和并行组合。n个行为通过应用一个n元函数(在静态值上)结合起来,"逐点",即一段时间内连续不断。好的。

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

    • 要想想出所有行为和事件都可以从中构建出来的组合词汇表,可以使用一些例子。继续分解成更一般/更简单的部分。好的。

    • 因此,你知道你是在坚实的基础上,给整个模型的组成基础,使用指称语义的技术,这只是意味着(a)每一种类型都有相应的简单和精确的数学类型的"意义",和(b)每个原语和运算符有一个简单的,精确的含义作为意义的函数O。f成分。永远不要将实现考虑因素混入您的探索过程中。如果这种描述对你来说是胡言乱语,请参考(a)使用类型类变形的表示设计,(b)推拉函数反应式编程(忽略实现位),以及(c)表示语义haskell wikibooks页面。注意,外延语义学有两个部分,分别来自其两位创始人克里斯托弗·斯特拉奇和达娜·斯科特:更容易和更有用的斯特拉奇部分和更难和不太有用的(软件设计)斯科特部分。好的。

    如果你坚持这些原则,我希望你会或多或少地从玻璃钢的精神中得到一些东西。好的。

    我从哪里得到这些原则?在软件设计中,我总是问同样的问题:"这意味着什么?".外延语义学为这个问题提供了一个精确的框架,一个符合我的美学(不同于操作语义学或公理语义学,两者都让我不满意)。所以我问自己什么是行为?我很快意识到命令式计算的时间离散性是对特定类型机器的适应,而不是对行为本身的自然描述。我能想到的对行为最简单的精确描述就是"时间的函数",所以这就是我的模型。令人高兴的是,该模型能够轻松、优雅地处理连续的、确定性的并发性。好的。

    正确有效地实现这个模型是一个很大的挑战,但这是另一回事。好的。好啊。


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

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

    反应式编程背后的基本思想是,"随着时间的推移"存在一些表示值的数据类型。涉及这些随时间变化的值的计算本身将具有随时间变化的值。

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

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

    在任何时候,x和y都有鼠标的坐标。与非反应式编程不同,我们只需要进行一次分配,X和Y变量将自动保持"最新"。这就是为什么反应式编程和函数式编程一起工作得那么好的原因:反应式编程消除了变量突变的需要,同时仍然让你做很多你可以用变量突变完成的事情。

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

    1
    2
    3
    4
    minX = x - 16;
    minY = y - 16;
    maxX = x + 16;
    maxY = y + 16;

    在这个例子中,minX总是比鼠标指针的x坐标小16。有了反应感知库,您就可以说:

    1
    rectangle(minX, minY, maxX, maxY)

    一个32x32的框将围绕鼠标指针绘制并跟踪它的移动位置。

    下面是一篇关于函数反应式编程的很好的论文。


    一个简单的方法来获得关于它是什么样子的第一直觉是想象你的程序是一个电子表格,你的所有变量都是单元格。如果电子表格中的任何单元格发生更改,则引用该单元格的任何单元格也会发生更改。玻璃钢也一样。现在想象一下,有些细胞自己发生了变化(或者更确切地说,来自外部世界):在GUI环境中,鼠标的位置就是一个很好的例子。

    这必然会漏掉很多。当你实际使用玻璃钢系统时,这个比喻很快就被打破了。首先,通常也会尝试对离散事件建模(例如单击鼠标)。我把这个放在这里只是想让你知道它是什么样子。


    对我来说,符号=有两种不同的含义:

  • 在数学中,x = sin(t)表示xsin(t)的不同名称。所以写x + ysin(t) + y是一样的。函数反应式编程在这方面类似于数学:如果编写x + y,则使用使用t的值进行计算。
  • 在C类编程语言(命令式语言)中,x = sin(t)是一种赋值:它表示x存储赋值时所取的sin(t)的值。

  • 好吧,从背景知识和阅读你所指的维基百科页面来看,反应式编程似乎类似于数据流计算,但是有特定的外部"刺激"触发一组节点来触发和执行它们的计算。

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

    具有该"体积值"节点边缘的各种节点将自动触发,任何必要的计算和更新将自然地在应用程序中波动。应用程序对用户刺激做出"反应"。功能性反应式编程只是用功能性语言实现这个概念,或者通常在功能性编程范式中实现。

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

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

    节点可以看作是并行触发,但通常是串行执行的,或者并行性有限(例如,可能有几个线程在执行它们)。一个著名的例子是曼彻斯特数据流机器,它(IIRC)使用一个标记的数据结构通过一个或多个执行单元来调度图中节点的执行。数据流计算非常适合于异步触发计算导致计算级联的情况,而不是试图让执行由时钟(或时钟)控制。

    反应式编程引入了这种"执行级联"的思想,似乎是以数据流的方式来考虑程序,但前提是一些节点被"外部世界"所吸引,当这些感觉类节点发生变化时,就会触发执行级联。然后,程序执行看起来就像是一个复杂的反射弧。程序可能基本上在刺激之间是无会话的,也可能在刺激之间进入基本无会话状态。

    "非反应式"编程将以一个非常不同的视角来编程,即执行流程和与外部输入的关系。这可能有点主观,因为人们可能会倾向于说出任何对外部输入做出"反应"的话。但是,从事物的本质来看,一个以固定的时间间隔对事件队列进行轮询并将发现的任何事件分派给函数(或线程)的程序反应性较低(因为它只处理固定时间间隔的用户输入)。同样,它也是这里的精神所在:我们可以想象将一个具有快速轮询间隔的轮询实现放在一个非常低级别的系统中,并以一种反应式的方式进行编程。


    在读了很多关于玻璃钢的文章之后,我终于看到了这篇关于玻璃钢的启迪性文章,它最终让我明白了玻璃钢到底是什么。

    我在下面引用海因里希·阿普非默斯(反应香蕉的作者)。

    What is the essence of functional reactive programming?

    A common answer would be that"FRP is all about describing a system in
    terms of time-varying functions instead of mutable state", and that
    would certainly not be wrong. This is the semantic viewpoint. But in
    my opinion, the deeper, more satisfying answer is given by the
    following purely syntactic criterion:

    The essence of functional reactive programming is to specify the dynamic behavior of a value completely at the time of declaration.

    For instance, take the example of a counter: you have two buttons
    labelled"Up" and"Down" which can be used to increment or decrement
    the counter. Imperatively, you would first specify an initial value
    and then change it whenever a button is pressed; something like this:

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

    The point is that at the time of declaration, only the initial value
    for the counter is specified; the dynamic behavior of counter is
    implicit in the rest of the program text. In contrast, functional
    reactive programming specifies the whole dynamic behavior at the time
    of declaration, like this:

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

    Whenever you want to understand the dynamics of counter, you only have
    to look at its definition. Everything that can happen to it will
    appear on the right-hand side. This is very much in contrast to the
    imperative approach where subsequent declarations can change the
    dynamic behavior of previously declared values.

    因此,在我的理解中,FRP程序是一组方程:enter image description here

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

    f依赖于t,因此这结合了模拟外部刺激的可能性。

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

    玻璃钢图书馆负责进行时间,换句话说,从jj+1

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

    编辑:

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

    x_is方程描述了一个依赖关系图。当一些x_ij时发生变化,那么不需要更新j+1的所有其他x_i'值,因此不需要重新计算所有依赖项,因为某些x_i'可能独立于x_i

    此外,可以增量更新做更改的x_i-s。例如,让我们考虑一个scala中的map操作f=g.map(_+1),其中fgIntsList。这里,f对应于x_i(t_j)g对应于x_j(t_j)。现在,如果我在g中预先准备一个元素,那么对g中的所有元素执行map操作是浪费的。一些玻璃钢实施(例如反射玻璃钢)旨在解决这个问题。这个问题也被称为增量计算。

    换句话说,FRP中的行为(x_is)可以被认为是缓存计算。如果某些f_i发生变化,则FRP引擎的任务是有效地使这些高速缓存(x_iS)失效并重新计算。


    本文简单介绍了Conal Elliott(Direct PDF,233-kb)的高效功能反应性。相应的库也可以工作。

    这篇论文现在被另一篇论文取代,即推拉函数式反应式编程(Direct PDF,286 KB)。


    免责声明:我的答案是在rx.js的上下文中——一个用于javascript的"反应式编程"库。

    在函数编程中,您不必遍历集合的每个项,而是将高阶函数(hofs)应用于集合本身。因此,FRP背后的想法是,不处理每个单独的事件,而是创建一个事件流(用一个可观察的*实现),并将HOF应用于该事件。通过这种方式,您可以将系统可视化为连接发布服务器和订阅服务器的数据管道。

    使用可观测数据的主要优点是:i)它从代码中抽象出状态,例如,如果您希望事件处理程序只针对每个"n"事件被激发,或者在第一个"n"事件之后停止激发,或者仅在第一个"n"事件之后开始激发,则可以使用hofs(分别使用filter、takeuntil、skip)而不是设置、更新和检查计数器。ii)它改善了代码的局部性——如果您有5个不同的事件处理程序改变了组件的状态,那么您可以合并它们的可观测项,并在合并的可观测项上定义单个事件处理程序,从而有效地将5个事件处理程序组合成1个。这使得很容易解释整个系统中哪些事件会影响组件,因为它都存在于单个处理程序中。

    • 可观测的是不可测的对偶。

    iterable是一个延迟使用的序列——每次迭代程序想要使用它时,它都会拉取每个项,因此枚举是由使用者驱动的。

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


    伙计,这主意太棒了!为什么我在1998年才发现这个?无论如何,这是我对弗兰教程的解释。建议是最受欢迎的,我正在考虑启动一个基于此的游戏引擎。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    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()

    简而言之:如果每个组件都可以被当作一个数字来处理,那么整个系统就可以被当作一个数学方程来处理,对吗?


    保罗·哈达克的书《哈斯克尔表达学派》不仅是哈斯克尔的一本很好的入门书,而且还花了相当多的时间在玻璃钢上。如果你是玻璃钢的初学者,我强烈建议你了解玻璃钢的工作原理。

    还有一个看起来像是这本书的新改写(2011年出版,2014年更新),哈斯克尔音乐学院。


    根据前面的答案,从数学上看,我们只是按照更高的顺序思考。我们不认为x的值是x的类型,而是考虑函数x:t→x,其中t是时间的类型,可以是自然数、整数或连续体。现在,当我们用编程语言写y:=x+1时,我们实际上是指方程y(t)=x(t)+1。


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

    就像所有的"范式"一样,它的新颖性也是有争议的。

    从我对参与者的分布式流网络的经验来看,它很容易成为节点网络中状态一致性的一般问题的牺牲品,也就是说,最终会在奇怪的循环中出现很多振荡和陷阱。

    这是很难避免的,因为某些语义意味着引用循环或广播,并且当参与者网络在某些不可预测的状态上聚合(或不聚合)时,可能会非常混乱。

    同样,尽管某些州有明确的边界,但可能无法达到这些州,因为全局状态会偏离解决方案。2+2可能是4,也可能不是4,这取决于2变成2的时间,以及它们是否保持这种状态。电子表格具有同步时钟和循环检测功能。分布式参与者通常不会。

    都很有趣:)。


    我在Clojure Subreddit上找到了这段关于玻璃钢的视频。即使你不认识Clojure,也很容易理解。

    视频如下:http://www.youtube.com/watch?V= NKET0K1RXU4

    下面是视频在下半部分中引用的源代码:https://github.com/cicayda/yolk-examples/blob/master/src/yolk-examples/client/autocomplete.cljs


    这篇安德烈·斯达茨的文章是我迄今为止所看到的最好和最清楚的解释。

    文章中的一些引用:

    Reactive programming is programming with asynchronous data streams.

    On top of that, you are given an amazing toolbox of functions to combine, create and filter any of those streams.

    以下是本文中精彩图表的一个示例:

    Click event stream diagram


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

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

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

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

    这大大减少了复杂性和调试时间。

    想想数学中a=b+c和程序中a=b+c的区别。在数学中,你描述的是一种永远不会改变的关系。在程序中,它说"现在"a是b+c。但是下一个命令可能是b++,在这种情况下a不等于b+c。在数学或声明性编程中,a总是等于b+c,不管你问什么时间点。

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

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

    行为是一个事件流+内存中的某个值。

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

    行为可以组成以产生新的行为,这是对其他n种行为的转换。此组合值将随着输入事件(行为)的触发而重新计算。

    "由于观察者是无状态的,我们通常需要其中的几个来模拟状态机,就像在拖动示例中那样。我们必须将状态保存到所有相关的观察者都可以访问的地方,例如上面的变量路径中。"

    引自-贬低观察者模式http://infoscience.epfl.ch/record/148043/files/deprecatingobserverstr2010.pdf


    对反应式编程的简短而清晰的解释出现在CycleJS-反应式编程中,它使用简单而直观的示例。

    A [module/Component/object] is reactive means it is fully responsible
    for managing its own state by reacting to external events.

    What is the benefit of this approach? It is Inversion of Control,
    mainly because [module/Component/object] is responsible for itself, improving encapsulation using private methods against public ones.

    它是一个良好的起点,而不是一个完整的知识来源。从那里你可以跳到更复杂和深入的论文。


    FRP是功能编程(建立在一切都是功能的概念之上的编程范式)和反应式编程范式(建立在一切都是流的概念之上(观察者和可观察哲学))的组合。它应该是世界上最好的。

    从反应式编程开始,看看安德烈·斯达茨的文章。


    查看RX,.NET的无功扩展。他们指出,使用IEnumerable,基本上就是从流中"拉"出来。对IQueryable/IEnumerable的LINQ查询是设置操作,用于"吸取"集合中的结果。但使用iobservable上的相同运算符,可以编写"react"的LINQ查询。

    例如,可以编写类似(从M开始,在Myobservablesetofmousemovements中其中m.x<100,m.y<100选择新点(m.x,m.y))。

    对于RX扩展,就是这样:当你在100100框中的时候,你的用户界面代码会对输入的鼠标移动流做出反应并绘制出来…