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

|▌冷眼眸甩不掉的悲伤 提交于 2019-12-11 17:29:50

我已经读过关于反应式编程的维基百科文章。 我还阅读了关于功能反应式编程的小文章。 描述非常抽象。

  1. 功能反应式编程(FRP)在实践中意味着什么?
  2. 反应式编程(与非反应式编程相反?)由什么组成?

我的背景是命令式/ OO语言,因此可以理解与此范例相关的解释。


#1楼

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

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

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

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

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

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

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

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


#2楼

论文Conal Elliott 简单有效的功能反应直接PDF ,233 KB)是一个相当不错的介绍。 相应的库也可以使用。

该论文现已被另一篇论文“ 推拉式功能反应式编程” 直接取代( 直接PDF ,286 KB)。


#3楼

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

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

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

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

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的盒子,无论它在哪里移动都会跟踪它。

这是关于功能性反应式编程的相当好的论文


#4楼

如果你想要了解FRP,你可以从1998年的旧Fran教程开始,该教程有动画插图。 对于论文,从功能反应动画开始,然后跟进我的主页上的出版物链接和Haskell维基上的FRP链接上的链接。

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

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

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

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

  • 动态/演化值(即,“随时间变化”的值)本身就是一等值。 您可以定义它们并将它们组合,将它们传入和传出函数。 我称这些事为“行为”。

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

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

  • 要想出可以构建所有行为和事件的组成词汇,请参考一些示例。 保持解构为更一般/更简单的部分。

  • 因此,你知道自己处于坚实的基础,使用指称语义技术给整个模型一个组合基础,这意味着(a)每种类型都有相应的简单和精确的“意义”数学类型,并且( b)每个原语和运算符具有作为组成部分含义的函数的简单和精确的含义。 永远不要将实施考虑因素混合到您的探索过程中。 如果这个描述对你来说是胡言乱语,请参考(a) 具有类型态射的指称设计 ,(b) 推挽功能反应式编程 (忽略实现位),以及(c) Denotational Semantics Haskell wikibooks页面 。 请注意,指称语义有两个部分,来自它的两位创始人Christopher Strachey和Dana Scott:更容易和更有用的Strachey部分以及更难和更少用途(用于软件设计)Scott部分。

如果你坚持这些原则,我希望你会在FRP的精神上获得或多或少的东西。

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

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


#5楼

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

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


#6楼

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

这本书的新改写(2011年发布,2014年更新), 哈斯克尔音乐学院也是如此


#7楼

对我而言,它有两种不同的符号含义=

  1. 在数学中x = sin(t)意味着, xsin(t) 不同名称 。 所以写x + ysin(t) + y是一回事。 在这方面,函数反应式编程就像数学一样:如果你写x + y ,它就是用它在使用时的t值来计算的。
  2. 在类C语言编程语言(命令式语言)中, x = sin(t)是赋值:它表示x存储赋值时获取的 sin(t)

#8楼

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

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

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


#9楼

如上所述,行为类似于电子表格。 通常基于事件驱动的框架。

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

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

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

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

一切都很好玩:)。


#10楼

免责声明:我的答案是在rx.js的背景下 - 一个用于Javascript的“反应式编程”库。

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

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

  • Observable是Iterable的双重性。

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

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


#11楼

在阅读了很多关于FRP的文章之后,我终于遇到了关于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程序是一组方程式:

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

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

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

FRP库负责进度时间,换句话说,将j带到j+1

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

编辑:

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

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

此外,可以逐步更新进行更改的x_i -s。 例如,让我们考虑Scala中的映射操作f=g.map(_+1) ,其中fgInts List 。 这里f对应于x_i(t_j)gx_j(t_j) 。 现在,如果我在前面加上一个元素g那么这将是浪费的,进行map操作中的所有元素g 。 一些FRP实现(例如reflex- frp)旨在解决此问题。 此问题也称为增量计算。

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


#12楼

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

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

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

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

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

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

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

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

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

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

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

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

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


#13楼

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


#14楼

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

例如,您可以编写一个Linq查询(从MyObservableSetOfMouseMovements中的m开始,其中mX <100且mY <100选择新Point(mX,mY))。

并且使用Rx扩展,就是这样:你有UI代码可以对输入的鼠标移动流作出反应,并在你进入100,100盒子时绘制...


#15楼

Andre Staltz撰写的这篇文章是迄今为止我见过的最好,最清晰的解释。

文章的一些引用:

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

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

以下是作为本文一部分的精彩图表示例:


#16楼

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

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

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

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


#17楼

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

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


#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()

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!