FRP也很不寻常,因为它是并发的,而不会违反困扰命令式并发的理论和实践的问题。 从语义上讲,FRP的并发是细粒度、确定性和连续性的。 (我说的是意义,而不是实现。一个实现可能涉及并发或并行,也可能不涉及。) 语义确定性对于推理(严格和非正式)非常重要。 虽然并发将使命令式编程变得非常复杂(由于非确定性交错),但在FRP中却是毫不费力的。
那么,FRP是什么? 你自己可能已经发明了它。 从这些想法开始:
动态/演化的值(即“随时间变化”的值)本身就是一等值。您可以定义它们并组合它们,将它们传递到和从函数中传递出来。我称这些东西为“行为”。
行为由一些基元组成,例如常量(静态)行为和时间(如时钟),然后使用顺序和并行组合。通过在静态值上连续地进行“点对点”n元函数应用来组合n个行为。
为了解释离散现象,还有另一种类型(系列)的“事件”,每个事件都有一系列(有限或无限)的发生。每次发生都有一个关联的时间和值。
为了找出所有行为和事件可以构建的组合词汇,可以尝试一些例子。保持分解成更通用/简单的部分。
为了确保您在坚实的基础上,使用指示语义的技术给整个模型提供组合基础,这意味着(a)每种类型都有相应的简单且精确的“含义”数学类型,以及(b)每个原始和运算符都具有简单且精确的意义作为成分含义的函数。您的探索过程中永远不要混合实现考虑因素。如果此描述对您来说是无意义的,请参阅(a)通过类型类态射进行指示性设计,(b)推-拉函数响应式编程(忽略实现部分),以及指示语义 Haskell wikibooks页面。请注意,指示语义有两个创始人Christopher Strachey和Dana Scott的两个部分:更容易且更有用的Strachey部分和更难且对软件设计不太有用的Scott部分。
在纯函数式编程中,不存在副作用。然而,在许多类型的软件中(例如任何用户交互的情况下),副作用在某个层面上是必要的。
在保留函数式风格的同时实现类似于副作用的行为的一种方法是使用函数响应式编程。这是函数式编程和响应式编程的结合体。(您链接的维基百科文章是关于后者的。)
响应式编程背后的基本思想是有某些数据类型表示值“随时间”变化。涉及这些随时间变化的值的计算本身也会随时间变化而产生值。
例如,您可以将鼠标坐标表示为整数-随时间变化的值对。假设我们有类似以下的东西(这是伪代码):
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的方框并在其周围跟踪它。
这里有一篇关于函数式反应编程的相当不错的论文。
sqrt(x)
,那就只会计算sqrt(mouse_x())
并返回一个双精度浮点数。在真正的函数响应式系统中,sqrt(x)
将返回一个新的“随时间变化的双精度浮点数”。如果你试图用#define
模拟FR系统,你几乎必须放弃使用变量,转而使用宏。FR系统通常只在需要重新计算时才进行重新计算,而使用宏意味着你将不断地重新评估所有表达式,一直到子表达式为止。 - Laurence Gonsalves一个简单的方法来直观地理解是将你的程序想象成一个电子表格,而所有的变量就是单元格。如果电子表格中的任何一个单元格发生变化,那些引用了该单元格的其他单元格也会随之改变。在FRP中也是一样的道理。现在想象一下,有些单元格会自动改变(或者说从外部世界获取):在GUI情况下,鼠标的位置就是一个很好的例子。
这个比喻肯定还有很多遗漏的地方。当你真正使用FRP系统时,这个比喻很快就会失效。首先,通常还会尝试对离散事件进行建模(例如鼠标点击)。我只是把这个放在这里是为了让你有个大概的了解。
=
有两个不同的意思:
x = sin(t)
的意思是x
是sin(t)
的另一个名称。因此,写x + y
就相当于写sin(t) + y
。在这方面,函数响应式编程就像数学一样:如果你写x + y
,它会根据使用时t
的值来计算。x = sin(t)
是赋值操作:它意味着x
存储在赋值时sin(t)
的值。x = sin(t)
意味着对于给定的t
,x
是sin(t)
的值。它 不是 sin(t)
函数的另一个名称。否则它将是x(t) = sin(t)
。 - Dmitri Zaitsev2 + 3 = 5
或 a**2 + b**2 = c**2
。 - user712092好的,从背景知识和你提供的维基百科页面阅读来看,反应式编程似乎类似于数据流计算,但是具有特定的外部"刺激"来触发一组节点进行计算。
这非常适合UI设计,例如在音乐播放应用程序中触摸用户界面控件(比如音量控制)可能需要更新各种显示项和实际音频输出的音量。当你修改音量(比如滑块)时,相应地也会修改指向有向图中节点关联的值。
与该"音量值"节点有边相连的各种节点会自动被触发,并且任何必要的计算和更新都会自然地涟漪传递到应用程序中。应用程序对用户刺激做出"反应"。函数式反应式编程只是在函数式语言或通常在函数式编程范例中实现这个思想。
更多关于"数据流计算"的内容,请在维基百科或您喜欢的搜索引擎上搜索这两个词。总体思路是:程序是一个由节点组成的有向图,每个节点执行一些简单的计算。这些节点通过图链接连接在一起,提供一些节点的输出作为其他节点的输入。
当一个节点触发或执行其计算时,连接到其输出的节点会"触发"或"标记"其对应的输入。任何所有输入都被触发/标记/可用的节点都会自动触发。根据反应式编程的具体实现方式,图可能是隐式的或显式的。
节点可以并行地触发,但通常它们是串行执行或受限的并行性(例如,可能有少量线程在执行它们)。一个著名的例子是曼彻斯特数据流机,它(如果我没记错的话)使用标记数据架构通过一个或多个执行单元调度图中节点的执行。数据流计算非常适合异步触发计算引起级联计算的情况,而不是尝试由时钟(或时钟)来管理执行。
响应式编程引入了“执行级联”的思想,将程序视为一种类似于数据流的方式,但前提是其中一些节点与“外部世界”相连,当这些感知类节点发生变化时就会触发级联执行。程序执行看起来就像复杂的反射弧一样。程序可能在刺激之间基本上是静止的,也可能在刺激之间进入基本稳态。
“非响应式”编程将具有非常不同的执行流和与外部输入的关系。这可能有点主观,因为人们很可能会倾向于说任何响应外部输入的东西都“响应”它们。但从整体上看,一个轮询事件队列并将找到的任何事件分派给功能(或线程)的程序会更少地响应(因为它只在固定时间间隔内处理用户输入)。再次强调这里的意图:可以想象在系统的非常低层级中使用快速轮询间隔的轮询实现,并在其上以响应式的方式进行编程。
经过阅读许多有关FRP的页面后,我终于发现了这篇关于FRP的启示性文章,它让我最终理解了FRP的真正含义。
以下是Heinrich Apfelmus(reactive banana的作者)的引述。
所以,在我理解中,FRP程序是一组方程: j是离散的:1、2、3、4... f取决于t,因此可以模拟外部刺激 程序的所有状态都封装在变量x_i中 FRP库负责推进时间,换句话说,将j带到j+1。 我在this视频中更详细地解释了这些方程。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:
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:
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.
x_i
的方程描述了一个依赖图。当一些x_i
在时间j
发生变化时,不是所有其他x_i'
值在j+1
时都需要更新,因此不需要重新计算所有依赖关系,因为某些x_i'
可能与x_i
无关。
x_i
可以进行增量更新。例如,在Scala中考虑一个映射操作f=g.map(_+1)
,其中f
和g
是Ints
列表。这里f
对应于x_i(t_j)
,而g
是x_j(t_j)
。现在,如果我在g
前添加一个元素,那么为了所有g
中的元素执行map
操作将是浪费的。一些FRP实现(例如reflex-frp)旨在解决这个问题。这个问题也被称为incremental computing.
换句话说,在FRP中,行为(x_i
)可以被视为缓存计算。如果一些f_i
发生变化,FRP引擎的任务就是有效地使这些缓存(x_i
)失效并重新计算它们。这篇名为简单高效的函数响应式编程的文章是由Conal Elliott撰写的(直接PDF链接,233 KB),是一个相当不错的介绍。相应的库也能够正常运行。
现在有一篇新的论文取代了这篇论文,它叫做推拉式函数响应式编程(直接PDF链接,286 KB)。
免责声明:我的回答是在rx.js上下文中的 - 一个用于Javascript的“响应式编程”库。
在函数式编程中,不需要遍历集合中的每个项目,而是将高阶函数(HoFs)应用于集合本身。因此FRP背后的想法是,创建事件流(使用observable*实现)而不是处理每个单独的事件,并将HoFs应用于该事件流。这样,您可以将系统视为将发布者连接到订阅者的数据管道。
使用observable的主要优点是:
i) 它从代码中抽象出状态,例如,如果您希望事件处理程序仅在每个'n'个事件后触发,或在第一次'n'个事件后停止触发,或仅在第一次'n'个事件后开始触发,则可以只使用HoFs(过滤器,takeUntil,skip等),而无需设置,更新和检查计数器。
ii) 它提高了代码局部性 - 如果您有5个不同的事件处理程序更改组件的状态,则可以合并它们的observable并定义单个事件处理程序在合并的observable上,有效地将5个事件处理程序合并为1个。这使得很容易推断出整个系统中哪些事件会影响组件,因为所有这些都存在于单个处理程序中。
Iterable是一个懒加载的序列 - 每个项目都是在迭代器随时想要使用它时由其拉取的,因此枚举由使用方驱动。
Observable是一个懒加载的生产者序列 - 每个项目都被推送到观察者中,只要它被添加到序列中,因此枚举由生产者驱动。
老兄,这真是个绝妙的点子!我为什么没在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()
简而言之:如果每个组件都可以被视为一个数字,那么整个系统就可以被视为一个数学方程,对吗?
Paul Hudak的书,《表达式的Haskell学校》,不仅是Haskell的一个很好的入门指南,而且还花了相当长的时间来介绍函数响应式编程(FRP)。如果你是FRP的初学者,我强烈推荐你阅读此书,以让你了解FRP的工作原理。
此外,还有一本看起来像是对这本书的新修订(2011年发布,2014年更新),《音乐的Haskell学校》。