functional-programming - ( 功能) 响应编程是什么?

  显示原文与译文双语对照的内容

我已经阅读了维基百科关于React式编程的文章。 我还阅读了关于功能React式编程的小文章。 描述相当抽象。

功能React式编程( 玻璃钢) 在实践中意味着什么? React式编程( 与non-reactive编程相对应) 由什么组成? 我的背景是命令/oo语言,因此与这里范例相关的解释将被欣赏。

时间:

如果你想了解玻璃钢,你可以先老弗兰教程从 1998,动画插图。 论文,开始功能活性动画然后跟进出版物的链接在我的主页上的链接和玻璃钢链接 Haskell wiki

就个人而言,我喜欢考虑 FRP 的含义,而不是它如何实现。 因此,我没有在表示/实现术语中描述 FRP,因为 Thomas K 在另一个答案( 图形,节点,边,激发,执行等) 中做了。 有多种可能的实现风格,但没有实现说明 FRP 是什么。

我对"表示值的数据类型'的简单描述做了共鸣,它是关于随时间而变化的'"。 常规的命令式编程只间接地通过状态和突变捕捉这些动态值。 完整的历史( 过去,现在,未来) 没有第一个类表示。 此外,只有离散进化值可以( 间接) 被俘,自从命令式范式是暂时的离散。 相比之下,玻璃钢捕捉这些变化值直接和没有困难不断发展的价值观。

玻璃钢也避免不寻常,并发afoul理论&务实困扰命令式并发老鼠的巢穴。 从语义上说,并发的玻璃钢 fine-grained, 决定性,连续 。 ( 我说的是意义,而不是实现) 。 一个实现可能包含并发性或者并行性。语义确定性对于推理来说非常重要,无论是严格的还是非正式的。 虽然并发性给命令式编程( 由于不确定的交错) 带来了极大的复杂性,但它在玻璃钢中毫不费力。

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

  • 动态/进化值( 例如,值"随着时间的推移") 是它们自身的类值。 你可以定义它们并将它们组合起来,将它们传递给&之外的函数。 我把这些东西叫做"行为"。

  • 行为由几个原语组成,比如常量( 静态) 行为和时间( 像一个时钟),然后是顺序和并行组合。 n 行为结合起来,将会随着时间应用一个( 在静态值上),"point-wise",换句话说,n-ary函数,进行化简。

  • 为了考虑离散现象,有另一个类型( 家族)"事件",其中每一个都有一个流( 有限或者无限) 。 每个事件都有相关的时间和值。

  • 要想找出构成所有行为和事件的组合词汇表,可以使用一些示例。 将分解为更一般/简单的Fragment 。

  • 好让你知道,是在坚实的地面,给整个模型具有可以组合基础之上,采用该技术的指称语义,这只意味着( 一个) 每一种类型都有相应的简单的"含义"&精确的数学类型,每个基元类型和运算符有一个简单&精确的意义,代表着函数的意思( b ) 所作的成分。 ,切勿混合的实现考虑事项为你的探索过程。 如果这对你描述是乱码,查阅( 一个) 指称设计与类型类态射,( b ) Push-pull函数式React性编程 ( 忽略实现位),并( c ) 指称语义接触Haskell维基教科书页 。 请注意,指称语义有两个部分,来自两个创始人 Christopher Strachey和 Dana Scott: 更容易的&更有用的Strachey部件和更困难的( 用于软件设计) Scott部件。

如果你坚持这些原则,我希望你能得到more-or-less的精神。

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

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

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

在保持功能风格的情况下,获得side-effect类似行为的一种方式是使用功能React式编程。 这是函数编程和React式编程的结合。 ( 你链接到的维基百科文章是关于后者的。)

React式编程背后的基本思想是,有一些数据类型代表一个值"一段时间"。 涉及这些changing-over-time值的计算本身会有随时间变化的值。

例如你可以将鼠标坐标表示为一对integer-over-time值。 假设我们有类似( 这是 pseudo-code )的东西:


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

在任何时刻,x 和y 都会有鼠标的坐标。 non-reactive编程,我们只需要把这个赋值一次,和x和y变量将自动保持"最新更新"。 这就是React式编程和函数编程协同工作得很好的原因: React性编程消除了需要改变变量同时还让你做很多你可以完成与变量突变。

如果我们在此基础上进行一些计算,结果值也会是随着时间变化的值。 例如:


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

在本例中,minX 将始终小于鼠标指针的x 坐标。 使用reactive-aware库,你可以说出如下内容:


rectangle(minX, minY, maxX, maxY)

和一个 32 x32格子会画鼠标指针周围并且跟踪它,无论它的行动。

这是一个非常好的论文,它关于函数式React式编程。

一种简单的方式达成的第一直觉就像什么是想象你的程序是一个电子表格,你所有的变量都是细胞。 如果电子表格中的任何单元格发生变化,则引用该单元格的任何单元格也会随之更改。 它与玻璃钢一样。 现在假设一些单元格在它们自己的( 或者是从外部获取的) 上改变: 在GUI场景中,鼠标的位置是一个很好的例子。

这一定会错过很多。 这个比喻在你实际使用一个玻璃钢系统时就会很快崩溃。 例如通常会尝试对离散事件建模( 例如。 点击鼠标。我只是把它放在这里,让你知道它是什么样子的。

好的,从背景知识和阅读维基百科页面来看,React式编程似乎是数据流计算,但是特定的外部"激励"触发了一个节点,触发了一个节点来激发和执行它们的计算。

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

来自"音量值"节点边缘的各种节点会自动被触发,任何必要的计算和更新都会自然地波及应用程序。 应用程序"反作用力"到用户刺激。 功能React式编程仅仅是在函数式语言中实现这个想法,或者在函数式编程范式中实现。

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

当节点激发或者执行它的计算时,连接到它的输出的节点有相应的输入"已经触发"或者"标记标记"。 任何具有所有输入 triggered/marked/available的节点都会自动激发。 根据React式编程的实现方式,图形可能是隐式的或者显式的。

节点可以并行方式看待,但它们通常是串行执行的,或者是使用有限的并行( 例如可能有几个线程执行它们) 。 一个著名的例子是曼彻斯特数据流机器,( IIRC ) 使用一个标记数据结构来通过一个或者多个执行单元来调度图形中节点的执行。 数据流计算非常适合于那些异步触发计算的情况,而这些计算级联比试图执行执行来控制执行更有效。

React性编程这"级联执行"想法和似乎认为进口的程序dataflow-like时尚但但书,一些节点连接"外部世界"和级联执行这些sensory-like节点改变时触发。 程序执行看起来类似于一个复杂的反射弧。 在刺激之间,程序可能基本上是无柄的,也可能在刺激之间变成基本无梗状态。

"non-reactive"编程将使用完全不同的执行流程和外部输入的关系进行编程。 它可能有点主观,因为人们可能会想说任何对外部输入"反作用力"做出响应的东西。 但是,看看这个事件的精神,一个在固定时间间隔轮询事件队列并分派任何事件到函数( 或者线程)的程序,它的React是不响应的。 同样,它是这里的精髓: 人们可以想象将轮询实现与快速轮询间隔放在一个非常低的级别,并在它上面以React式方式运行。

对我来说它是关于 2 = 象征不同的含义:

  1. 在数学 x = sin(t) 意味着, xsin(t)不同的名称。 所以写 x + ysin(t) + y 一样。 功能React式编程在这方面类似于数学: 如果你写 x + y,它是用 t的值计算的。
  2. 在C-like编程语言( 命令式语言) 中,x = sin(t) 是一个赋值: 这意味着 x 商店的价值sin(t) 拍摄时的任务。

文件将短期机体的高效 Conal Elliott ( 直接 PDF,233 KB ) is a good导言这一点。 相应的库也可以。

论文现在被另一篇论文取代,Push-pull功能React式编程 ( 直接 PDF,286 KB ) 。

伙计,这真是个绝妙的主意 ! 为什么我在 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()

简而言之:如果每个组件都可以当作一个数字对待,整个系统就可以像数学等式一样被处理,对吧?

在阅读许多关于玻璃钢的页面我终于遇到启发写玻璃钢,终于让我明白玻璃钢真的。

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

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

一个常见的答案是"玻璃钢是指用time-varying函数代替可变状态来描述系统",这当然不是错误的。 这是语义观点。 但在我看来,更深层,更令人满意的答案是由以下纯粹的句法标准给出的:

功能React式编程的本质是在声明时完全指定一个值的动态行为。

例如以计数器为例: 你有两个标签为"向上"和"向下"的按钮,可以用来增加或者减少计数器。 强制地,首先指定一个初始值,然后在按下按钮时更改它;类似于:


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

要点是在声明时,只指定计数器的初始值;计数器的动态行为在程序文本的其余部分隐式。 相反,功能React编程在声明时指定整个动态行为,如下所示:


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

每当你想了解计数器的动态时,你只需要查看它的定义。 它会出现在右边的一切。 这与命令式方法非常有区别,因为后面的声明可以改变以前声明的值的动态行为。

所以,在我的理解玻璃钢程序是一组方程: enter image description here

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

f 依赖于 t,因此它将possiblilty与外部刺激建模

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

玻璃钢库负责处理时间,换句话说,把 j 带到 j+1

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

hudak的Paul book,表达式的Haskell学派,它不仅是Haskell的优秀介绍,而且它也花费了大量的时间在FRP上。 如果你是FRP的初学者,我强烈建议你了解玻璃钢是如何工作的。

免责声明:我的答案在 rx.js的上下文中- 一个用于Javascript的玻璃钢库。

在函数编程中,你可以对集合应用高阶函数( HoFs ),而不是遍历集合中的每一项。 因此,玻璃钢背后的思想是,不处理每个单独的事件,而是创建一个事件流( 用observable*实现) 并将HoFs应用到它。 通过这种方式,你可以将系统可视化为将发布服务器连接到订阅服务器的数据管道。

使用可以观察的主要优点是:
我)它从你的系统,消除了国家 比如, 如果你想让事件处理程序只对每个'n'th事件被解雇,或停止射击第一次'n'事件后,第一'n'火灾事件,你可以使用 HoFs ( 筛选器,takeUntil,跳过) 而设置,更新和检查柜台。
ii ) 它负责在完成后释放内存,这样你就不必去检查是否记住了所有的事件监听器。
iii ) 改进了代码的局部性- 如果你有 5个不同的事件处理程序改变了一个组件的状态,你可以在合并的观察中合并一个事件处理程序,有效地将 5事件处理程序合并为 1. 这使得你很容易理解整个系统中哪些事件会影响组件,因为它们都在一个单一的处理程序中。

  • 可以观察的是Iterable的异步对偶:
    1. myIterable.forEach(myFunc) <--> myObservable.subscribe(myFunc )
    2. 同步,调用方被阻塞,不能继续到下一个操作直到Iterable处理整个集合 <--> 异步调用,调用者没有被阻塞,调用方不会等待整个集合处理
...