什么是(function性)反应式编程?
我已阅读维基百科有关反应式编程的文章。 我也读过关于function性反应式编程的小文章。 描述非常抽象。
- function性反应式编程(FRP)在实践中意味着什么?
- 反应式编程(与非反应式编程相反)包括什么?
我的背景是在命令/面向对象的语言,所以有关这种范式的解释将不胜感激。
如果你想感受一下FRP,你可以从1998年的老弗朗教程开始,那里有animation插图。 对于论文,从Functional Reactive Animation开始,然后跟随我的主页上的出版物链接和Haskell wiki上的FRP链接。
就我个人而言,在讨论如何实施之前,我想先考虑一下玻璃钢的含义 。 (没有规范的代码是一个没有问题的答案,因此“甚至没有错”)所以我没有用expression/实现术语来描述FRP,正如Thomas K在另一个答案(graphics,节点,边缘,射击,执行,等等)。 有许多可能的实现方式,但没有实现说明什么是FRP。
我对劳伦斯G的简单描述产生了共鸣,即玻璃钢是关于“随着时间的推移而代表价值的数据types”。 传统的命令式编程仅通过状态和突变间接地捕获这些dynamic值。 完整的历史(过去,现在,未来)没有一stream的代表性。 此外,由于必要的范式是暂时离散的,因此只能(间接)捕获离散演变的值。 相比之下,FRP 直接捕捉这些演变的价值观,并且不断地发展价值。
FRP也是不寻常的,因为它同时不会与理论和实用的老鼠窝发生冲突,而造成命令式的并发。 在语义上,FRP的并发性是细化的 , 确定的和连续的 。 (我是在谈论意义,而不是实现,实现可能涉及或不涉及并发或并行)。语义确定性对于推理是非常重要的,无论是严谨的还是非正式的。 虽然并发性增加了命令式编程(由于非确定性交错)的巨大复杂性,但它在FRP中是毫不费力的。
那么,什么是玻璃钢? 你可以自己发明它。 从这些想法开始:
-
dynamic/演变的价值观(即“随着时间的推移”)本身就是一stream的价值观。 您可以定义它们并将它们合并,将它们传入和传出函数。 我把这些东西叫做“行为”。
-
行为是由几个基本元素组成的,比如常量(静态)行为和时间(如时钟),然后是顺序和并行组合。 n行为通过应用n元函数(基于静态值),“逐点”(即,随着时间的推移连续)而被组合。
-
为了解释离散的现象,有另一种types(家庭)的“事件”,其中每一个都有一个事件stream(有限的或无限的)。 每个事件都有相关的时间和价值。
-
想出所有的行为和事件都可以构build出来的构图词汇,玩一些例子。 保持解构成更一般/简单的作品。
-
为了让你知道自己在坚实的基础上,使用指称语义学的技术给整个模型一个构图的基础,这就意味着(a)每种types都有相应的简单精确的mathtypes的“意义”,而b)每个原语和操作符都具有简单和精确的含义,作为成分的含义的函数。 永远不要将执行考虑因素混合到您的探索过程中。 (a) 使用types态射的指称devise ,(b) 推挽function反应式编程 (忽略实现位),以及(c) 指示语义 Haskell wikibooks页面 。 请注意,指称语义由两个创始人克里斯托弗·斯特拉奇(Christopher Strachey)和达纳·斯科特(Dana Scott)两部分组成:更简单,更实用的斯特拉奇部分,以及斯科特部分更难和更less用的软件devise。
如果你坚持这些原则,我希望你会在玻璃钢的精神上得到或多或less的好处。
我在哪里得到这些原则? 在软件devise中,我总是问同样的问题:“这是什么意思? 指称语义为这个问题提供了一个精确的框架,并且符合我的美学(与操作语义或公理语义不同,这两者都使我不满意)。 所以我问自己什么是行为? 我很快意识到,命令式计算的时间离散性是对特定机器风格的适应,而不是对行为本身的自然描述。 我能想到的最简单的行为的精确描述仅仅是“(连续)时间的函数”,所以这就是我的模型。 令人高兴的是,这个模型轻松和优雅地处理连续的,确定性的并发。
正确高效地实施这个模型是相当大的挑战,但这是另一回事。
在纯函数式编程中,没有任何副作用。 对于许多types的软件(例如,与用户交互的任何事物),在某种程度上需要副作用。
获得类似副作用的行为同时保留function性风格的一种方法是使用function性反应式编程。 这是function编程和反应式编程的结合。 (你链接到的维基百科文章是关于后者。)
反应式编程背后的基本思想是,有一些数据types代表一个值“随着时间的推移”。 涉及这些转换时间价值的计算本身将具有随时间变化的价值。
例如,您可以将鼠标坐标表示为一对整数时间值。 假设我们有类似的东西(这是伪代码):
x = <mouse-x>; y = <mouse-y>;
在任何时候,x和y都会有鼠标的坐标。 与非反应式编程不同的是,我们只需要一次赋值,而x和yvariables将自动保持“最新”。 这就是为什么反应式编程和函数式编程很好地结合在一起的原因:反应式编程消除了变异变异的需要,同时让您用变异性突变来完成许多事情。
如果我们根据这个做一些计算,结果值也会随着时间而变化。 例如:
minX = x - 16; minY = y - 16; maxX = x + 16; maxY = y + 16;
在这个例子中, minX
总是比鼠标指针的x坐标小16。 有了反应感知库,你可以这样说:
rectangle(minX, minY, maxX, maxY)
鼠标指针周围将绘制一个32×32的框,并在任何移动的位置进行跟踪。
这是关于function性反应式编程的一个很好的论文 。
想象一下你的程序是一个电子表格,你所有的variables都是单元格。 如果电子表格中的任何单元格更改,则引用该单元格的单元格也会更改。 这与FRP一样。 现在想象一下,有些细胞会自行改变(更确切地说,是从外部世界中获取的):在GUI情况下,鼠标的位置就是一个很好的例子。
这肯定会错过很多。 当你实际使用FRP系统的时候,这个比喻非常快。 首先,通常也会尝试模拟离散事件(例如点击鼠标)。 我只是在这里给你一个想法是什么。
对我来说,它大约有两种不同的符号=
含义:
- 在math
x = sin(t)
意思是,x
是sin(t)
不同名称 。 所以写x + y
和sin(t) + y
。 function反应式编程在这方面就像math:如果你写x + y
,它就是用它在使用的时候t
的值来计算的。 - 在类C编程语言(命令式语言)中,
x = sin(t)
是一个赋值:这意味着x
存储了赋值时sin(t)
的值 。
好的,从背景知识和阅读你指出的维基百科页面看来,反应式编程似乎是数据stream计算,但是具有特定的外部“刺激”,触发一组节点来触发并执行其计算。
这非常适合于UIdevise,例如,触摸用户界面控件(比如音乐播放应用程序中的音量控制)可能需要更新各种显示项目和audio输出的实际音量。 当你修改音量(一个滑块,比方说),这将对应于修改与有向图中的节点相关联的值。
具有来自该“音量值”节点的边缘的各种节点将自动被触发,并且任何必要的计算和更新自然会波及整个应用。 应用程序“反应”到用户的刺激。 functionreact native编程只是在function语言中实现这个想法,或者通常在function性编程范例中。
有关“数据stream计算”的更多信息,请在维基百科上search这两个词或使用您最喜爱的search引擎。 总体思路是这样的:程序是节点的有向图,每个节点执行一些简单的计算。 这些节点通过graphics链接相互连接,从而将某些节点的输出提供给其他节点的input。
当节点触发或执行计算时,连接到其输出的节点将其相应的input“触发”或“标记”。 所有触发/标记/可用的input的节点都会自动触发。 该图可能是隐式的或显式的,具体取决于如何实现反应式编程。
可以将节点视为并行触发,但通常是串行执行或有限并行执行(例如,可能有几个执行它们的线程)。 一个着名的例子是曼彻斯特数据stream机器 ,它(IIRC)使用标记数据体系结构通过一个或多个执行单元来调度图中节点的执行。 数据stream计算非常适合于触发计算asynchronous产生级联计算的情况比试图由时钟(或多个时钟)控制执行更好的情况。
反应式编程引入了这种“执行级联”思想,似乎是以类似于数据stream的方式来思考程序,但是其中一些节点被挂钩到“外部世界”,并且当这些感觉被触发时,触发级联像节点改变。 程序的执行看起来就像是一个复杂的reflection弧。 这个程序可能在刺激之间基本上是固定的,或者可能在刺激之间基本处于静止状态。
“非react native”的编程可以用非常不同的执行stream程和与外部input的关系来编程。 这很可能是有些主观的,因为人们可能会试图说任何对外部投入做出反应的东西会“对它们做出反应”。 但是看看事物的精神,一个以固定的时间间隔轮询一个事件队列并且发现对函数(或线程)发现的任何事件的程序反应较less(因为它只以固定的时间间隔参与用户input)。 再一次,这就是这个事情的精神:可以想象,把一个轮询间隔快的轮询实现放到一个非常低的系统中,并且以一种反应式的方式进行编程。
在阅读了许多有关玻璃钢的网页之后,我终于看到了这篇关于玻璃钢的启发性文章,最终让我明白了玻璃钢的真正意义。
我引用海因里希Apfelmus(反应香蕉的作者)下面。
function性反应式编程的本质是什么?
一个普遍的答案是,“FRP就是用时变函数来描述一个系统,而不是可变的状态”,这当然不是错的。 这是语义观点。 但在我看来,更深层的,更令人满意的答案是由以下纯粹的句法标准给出的:
function性反应式编程的实质是在声明时完全指定一个值的dynamic行为。
例如,以计数器为例:您有两个标记为“上”和“下”的button,可用于递增或递减计数器。 必须先指定一个初始值,然后每当按下一个button时就改变它。 像这样的东西:
counter := 0 -- initial value on buttonUp = (counter := counter + 1) -- change it later on buttonDown = (counter := counter - 1)
重点是在申报时,只规定了柜台的初始值; 计数器的dynamic行为隐含在程序文本的其余部分。 相比之下,function性的反应式编程指定了声明时的整个dynamic行为,如下所示:
counter :: Behavior Int counter = accumulate ($) 0 (fmap (+1) eventUp `union` fmap (subtract 1) eventDown)
每当你想了解计数器的dynamic,你只需要看它的定义。 所有可能发生的事情都将出现在右侧。 这与后面的声明可以改变之前声明的值的dynamic行为的命令式方法形成鲜明对比。
所以,根据我的理解 ,FRP程序是一组等式:
j
是离散的:1,2,3,4 …
f
取决于t
所以这个模型包含了模拟外部刺激的可能性
程序的所有状态都封装在variablesx_i
FRP库负责处理进度时间,换句话说,把j
加到j+1
。
我在这个video中更详细地解释这些方程式。
编辑:
最初的答案大约2年后,最近我得出的结论是FRP的实施还有另外一个重要方面。 他们需要(通常是)解决一个重要的实际问题: caching失效 。
x_i
s的等式描述依赖图。 当某些x_i
在时刻j
发生变化时,并不需要更新j+1
所有其他x_i'
值,因此不需要重新计算所有依赖关系,因为某些x_i'
可能与x_i
无关。
此外,可以逐渐更新x_i
-s。 例如,让我们考虑在Scala中的映射操作f=g.map(_+1)
,其中f
和g
是Ints
List
。 这里f
对应于x_i(t_j)
并且g
是x_j(t_j)
。 现在,如果我将一个元素添加到g
那么对g
中的所有元素执行map
操作将是浪费的。 一些FRP实现(例如reflex- frp)旨在解决这个问题。 这个问题也被称为增量计算。
换句话说,FRP中的行为( x_i
s)可以被认为是caching计算。 FRP引擎的任务是,如果某些f_i
s确实发生了变化,则有效地对这些caching( x_i
-s)进行无效化和重新计算。
Conal Elliott的简单有效的function反应 ( 直接PDF ,233 KB)是一个相当好的介绍。 相应的库也可以工作。
这篇论文现在已经被另一篇论文– 推挽function反应式编程 ( 直接PDF ,286 KB)取代。
免责声明:我的回答是在rx.js的上下文中 – 一个用于Javascript的“react native编程”库。
在函数式编程中,不是迭代集合中的每个项目,而是将高阶函数(HoFs)应用于集合本身。 所以FRP背后的想法是,不是处理每个单独的事件,而是创build一个事件stream(用一个可观察的*实现),然后将HoF应用到这个事件上。 这样,您可以将系统可视化为将发布者连接到订阅者的数据pipe道。
使用可观察的主要优点是:
i)它将代码中的状态抽象出来,例如,如果你想让事件处理器只为每一个“n”事件触发,或者在第一个“n”事件之后停止触发,或者在第一个“n”事件之后才开始触发'事件,你可以使用HoFs(filter,takeUntil,skip分别)而不是设置,更新和检查计数器。
ii)它改善了代码的局部性 – 如果你有5个不同的事件处理器改变了一个组件的状态,你可以合并它们的observable,并在合并的observable上定义一个事件处理器,而将5个事件处理器有效地组合成1。很容易推断出整个系统中的事件会影响组件,因为这些事件都存在于一个处理程序中。
- 可观察是Iterable的双重性。
一个Iterable是一个懒惰的消费序列 – 每一个元素都被迭代器随时拉取,因此枚举是由消费者驱动的。
一个可观察对象是一个懒惰的生成序列 – 每当将其添加到序列中时,每个项目都被推送给观察者,因此枚举由生产者驱动。
伙计,这是一个怪胎的好主意! 为什么我在1998年没有发现这件事? 无论如何,这是我对Fran教程的解释。 build议是最受欢迎的,我正在考虑启动一个基于此的游戏引擎。
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()
简而言之:如果每个组件都可以像一个数字一样对待,那么整个系统就可以像一个math方程一样对待,对吗?
Paul Hudak的书“哈斯克尔expression学派”不仅是对Haskell的一个很好的介绍,而且在FRP上也花费了相当长的时间。 如果您是FRP的初学者,我强烈build议您了解FRP的工作原理。
Haskell音乐学院还有一本新的重写书(2011年发布,2014年更新)。
根据以前的答案,在math上看来,我们只是想得更高。 我们不考虑一个具有Xtypes的值x ,而是考虑一个函数x : T → X ,其中T是时间types,不pipe是自然数,整数还是连续性。 现在当我们在编程语言中写y := x + 1时,我们实际上是指方程y ( t )= x ( t )+1。
我在关于FRP的Clojure subreddit上find了这个不错的video。 即使你不知道Clojure也很容易理解。
以下是video: http : //www.youtube.com/watch?v =nket0K1RXU4
下面是video在下半部分提到的源代码: https : //github.com/Cicayda/yolk-examples/blob/master/src/yolk_examples/client/autocomplete.cljs
像注明的电子表格一样行事。 通常基于事件驱动的框架。
与所有的“范式”一样,新颖性是值得商榷的。
根据我对演员的分布式streamnetworking的经验,它很容易陷入节点networking状态一致性的一般问题,即最终会出现大量的振荡和陷入奇怪的循环。
这是很难避免的,因为一些语义意味着引用循环或广播,并且可能相当混乱,因为演员的networking在某种不可预测的状态上会聚(或不会)。
类似地,尽pipe边界有明确的定义,但有些状态可能无法到达,因为全局状态远离解决scheme。 2 + 2可能会或可能不会成为4取决于2成为2的时间,以及他们是否保持这种方式。 电子表格具有同步时钟和循环检测。 分布式演员通常不会。
所有好玩:)。
Andre Staltz的这篇文章是迄今为止我见过的最好,最清晰的解释。
文章中的一些引用:
反应式编程是用asynchronous数据stream编程的。
最重要的是,您将得到一个令人惊叹的函数工具箱,可以组合,创build和过滤任何这些stream。
这里是一个梦幻般的图表的例子是文章的一部分:
这是关于随着时间的推移(或忽略时间)的math数据转换。
在代码中,这意味着function纯度和声明式编程。
国家臭虫在标准命令范例中是一个巨大的问题。 代码的各种位可能会在程序执行的不同“时间”上改变某些共享状态。 这很难处理。
在FRP中,您描述了数据如何从一个状态转换到另一个状态,以及触发它的方式(如在声明性编程中)。 这可以让你忽略时间,因为你的function只是简单地对input做出反应,并用当前的值来创build一个新的。 这意味着状态被包含在转换节点的graphics(或树)中,并且在function上是纯的。
这大大降低了复杂性和debugging时间。
考虑math中的A = B + C和程序中的A = B + C之间的差异。 在math中,你正在描述永不改变的关系。 在一个程序中,它说“现在”A是B + C。 但是下一个命令可能是B ++,在这种情况下A不等于B + C。 在math或陈述式编程中,无论你问什么时候,A总是等于B + C。
所以通过消除共享状态的复杂性和随时间变化的价值。 你的程序很容易理解。
一个EventStream是一个EventStream +一些转换函数。
行为是一个EventStream +内存中的一些值。
事件触发时,通过运行转换函数更新值。 这产生的价值存储在行为记忆。
行为可以被组合来产生新的行为,这是对其他行为的一种转变。 这个组合的值将在input事件(行为)激发时重新计算。
“由于观察者是无状态的,我们经常需要其中的几个来模拟一个状态机,就像拖动的例子一样,我们必须保存所有相关观察者都可以访问的状态,比如上面的variablespath。
引用 – 弃用观察者模式UTF-8''DeprecatingObserversTR2010.pdf
关于反应式编程的简短而清晰的解释出现在Cyclejs – 反应式编程中 ,它使用简单和可视化的样本。
一个[模块/组件/对象] 是被动的,意味着它完全负责通过对外部事件的反应来pipe理自己的状态。
这种方法的好处是什么? 这是控制反转 ,主要是因为[module / Component / object]自己负责,用私有方法改善封装。
这是一个很好的起点,而不是知识的完整来源。 从那里你可以跳到更复杂和更深的论文。
查看Rx,.NET的Reactive Extensions。 他们指出,IEnumerable基本上是从stream中“拉”出来的。 对IQueryable / IEnumerable进行的Linq查询是集合操作,它将结果从一个集合中抽取出来。 但是使用IObservable上的相同运算符,您可以编写“反应”的Linq查询。
例如,你可以编写一个Linq查询,例如(从MyObservableSetOfMouseMovements中的m,其中mX <100,mY <100select新的Point(mX,mY))。
和Rx扩展,就是这样:你有UI代码,对input的鼠标移动stream作出反应,并在100,100盒子中绘制。