功能反应编程简介

来源:互联网 发布:手机版我的世界工业js 编辑:程序博客网 时间:2024/05/01 00:07

功能反应编程简介
2017年7月27日 在谈话
我今年发表了一个关于功能反应规划(FRP)的演讲,试图分解给予FRP的名称以及为什么要关心。这是一个谈话的写作。

介绍
功能反应式编程在过去几年一直是愤怒的。但是呢,究竟是什么呢?你为什么要关心?

即使对于正在使用像RxJava这样的FRP框架的人来说,FRP背后的根本原因也许是神秘的。我今天要打破这个谜,通过将FRP分解成各自的组件:反应式编程和功能编程。

反应式编程

首先,我们来看看编写无效代码的意思。

我们从一个简单的例子开始:一个开关和一个灯泡。当您滑动开关,灯泡打开和关闭。

在编码方面,两个组件是耦合的。通常你并没有太多思考它们如何耦合,而是让我们深入研究。

一种方法是使开关修改灯泡的状态。在这种情况下,交换机是主动的,将新状态推向灯泡; 而灯泡是被动的,只需接收命令来改变其状态。

我们将通过将箭头放在交换机上来表示这种关系 - 也就是说,连接两个组件的是交换机,而不是灯泡。

这是一个主动解决方案的草图:它Switch包含一个实例,LightBulb然后在状态改变时进行修改。

耦合这些组件的另一种方法是使灯泡听到开关的状态,然后相应地进行修改。在这个模型中,灯泡是无功的,根据开关的状态改变其状态; 而交换机是可观察的,因为其他的可以观察其状态变化。

这是一个反应式解决方案的草图:它LightBulb接收一个Switch它侦听事件,然后根据侦听器修改自己的状态。

对于最终用户来说,主动和反应代码都会产生相同的结果。每种方法有什么区别?

第一个区别是谁控制LightBulb。在主动模式中,它必须是一些调用的外部组件LightBulb.power()。具有反应性,它LightBulb本身就控制其亮度。

第二个区别是谁决定什么是Switch控制。在主动模式下,Switch本身决定了它控制谁。在反应模型中,它Switch是无知的,它的驱动,因为其他人通过听众钩入它。

你得到这两个模型是相互镜像的感觉。主动和反应性编码之间存在对偶性。

然而,两个组件之间的紧密或松散耦合有微妙的差异。在主动模式中,模块直接相互控制。在反应模型中,模块控制自身并间接挂钩。

让我们来看看这在现实生活中如何发挥。这是Trello主屏幕。它显示了您从数据库中获得的主板。这种关系如何用主动或反应模式发挥出来?

使用主动模式,每当数据库更改时,它会将这些更改推送到UI。但这没有任何意义:为什么我的数据库必须关心UI?为什么要检查主屏幕是否正在显示,并且知道是否应该将新数据推送到它?主动模式在我的数据库和我的UI之间创建了一个非常紧密的联系。

相比之下,反应模型更清洁。现在我的UI听取数据库中的更改,必要时进行更新。数据库只是提供监听器的一个愚蠢的知识库。任何人都可以更新这些更改,只需在UI中将其进行更改即可。

这是好莱坞的行动原则:不要打电话给我们,我们会打电话给你。而且,松散耦合代码非常好,可以封装您的组件。

我们现在可以回答什么是反应式编程:这是当您专注于使用无效代码的时候,而不是默认的主动代码。

尽管如此,如果您希望默认为反应式,我们的简单听众也不会很好。有几个问题:

首先,每个听众都是独一无二的。我们有Switch.OnFlipListener,但这只能用Switch。可以观察的每个模块都必须实现自己的监听器设置。这不仅意味着一堆忙碌的实施样板代码,这也意味着您不能重复使用反应式模式,因为没有建立常见的框架。

第二个问题是每个观察者都必须直接访问可观察的组件。在LightBulb具有直接访问Switch,以便开始倾听。这导致模块之间的紧密耦合,这破坏了我们的目标。

我们真正想要的是如果Switch.flips()返回一些可以传递的广义类型。让我们弄清楚我们能够满足我们需求的什么类型的回报。

函数可以返回四个基本对象。在一个轴上是返回多少个项目:单个项目或多个项目。另一方面是项目是立即返回(同步)还是项目表示将要稍后传递的值(异步)。

同步返回很简单。单个返回的类型为T:任何对象。同样,多个项目只是一个Iterable。

使用同步代码进行编程很简单,因为您可以在获取它们时开始使用返回的值,但是我们不在这个世界。反应编码本质上是异步的:没有办法知道可观察组件何时会发出新的状态。

因此,我们来检查异步返回。一个异步项目相当于Future。好,但不完全是我们想要的 - 可观察的组件可能有多个项目(例如,Switch可以多次打开/关闭)。

我们真正想要的是在右下角。我们要调用,去年象限是Observable。A Observable是所有反应框架的基础。

让我们来看看效果如何Observable。在我们的新代码中,Switch.flips()返回一个Observable代表状态的真/假序列Switch。现在我们LightBulb,而不是Switch直接消费,将订阅Observable该Switch提供。

此代码的行为与我们的非Observable代码相同,但是修复了我之前概述的两个问题。Observable是一种广泛的类型,允许我们建立它。它可以传递,所以我们的组件不再紧密耦合。

让我们巩固一下什么的基础Observable。An Observable是一段时间内的项目集合。

我在这里展示的是大理石图。该行代表时间,而圆圈表示Observable将推送给订阅者的事件。

Observable 可能会导致两个可能的终端状态之一:成功完成和错误。

大理石图中的垂直线表示成功完成。并不是所有的收藏都是无限的,有必要能够代表这一点。例如,如果您在Netflix上流式传输视频,则某些视频将会结束。

错误由X表示,并且是由于某些原因数据流变得无效的结果。例如,如果有人把大锤带到我们的切换点,那么值得通知大家,我们的切换不仅停止了发布任何新的状态,而且还因为破坏而听不到任何声音。

功能编程

让我们放一下反应式编程,然后跳入功能性编程中。

功能编程侧重于功能。呃对吧?嗯,我不是说任何一个简单的老功能:我们正在做纯功能的烹饪。

让我通过反例来解释一个纯粹的功能。

假设我们有这个完美合理的add()功能,它将两个数字加在一起。但等等,功能中的所有空白空间是什么?

哎呀!它add()发送一些文本到stdout。这就是所谓的副作用。目标add()是不要打印到stdout; 它是添加两个数字。然而,它正在修改应用程序的全局状态。

但等等,还有更多。

哎哟! 它不仅打印到stdout,而且还会杀死程序。如果你所做的只是看功能定义(两个int中,一个int out),你不知道使用这种方法会对你的应用程序造成什么破坏。

我们来看另一个例子。

在这里,我们将列出一个列表,看看元素的总和是否与产品相同。我会认为这是真的,[1, 2, 3]因为1 + 2 + 3 == 6和1 * 2 * 3 == 6。

但是,请查看该sum()方法的实现方式。它不影响我们的应用程序的全局状态,但它确实修改了其中的一个输入!这意味着代码将失火,因为在product(numbers)运行的时候,numbers将是空的。尽管如此,这种问题可以在实际的,不纯的功能中一直存在。

随时更改功能外部的状态时会发生副作用。如您所见,副作用可能使编码困难。纯功能不允许任何副作用。

有趣的是,这意味着纯函数必须返回一个值。具有返回类型void将意味着纯函数将不会执行任何操作,因为它不能修改其输入或函数之外的任何状态。

这也意味着您的函数的输入必须是不可变的。我们不能允许输入是可变的; 否则并发代码可以在函数执行时更改输入,从而打破纯度。顺便提一句,这也意味着输出也应该是不可变的(否则不能作为其他纯函数的输入)。

纯函数有一个第二个方面,就是给出相同的输入,它们必须始终返回相同的输出。换句话说,它们不能依赖于任何外部状态的功能。

例如,查看这个打给用户的功能。它没有任何副作用,但随机返回两个问候之一。随机性通过外部静态功能提供。

这使得编码变得更加困难,有两个原因。首先,无论您的输入如何,该功能都有不一致的结果。如果您知道与函数相同的输入导致相同的输出,那么对代码的理解要容易很多。其次,你现在在函数中有一个外部依赖; 如果外部依赖关系以任何方式改变,该函数可能开始行为不同。

面向对象开发人员可能会混淆的是,这意味着纯函数甚至不能访问其中包含的类的状态。例如,Random方法本质上是不纯的,因为它们基于Random内部状态在每个调用上返回新的值。

简而言之:功能编程是基于纯函数。纯粹的功能是不消耗或突变外部状态的功能 - 它们完全依靠输入来获得输出。

经常碰到人们首先引入FP的一点混乱:你怎么突变什么?例如,如果我想获取一个整数列表并将其所有值都加倍呢?当然你必须变更名单,对吧?

嗯,不完全 您可以使用一个纯粹的功能来转换您的列表。这是一个纯粹的功能,使列表的值翻倍。没有副作用,没有外部状态,没有输入/输出的突变。这个功能可以让脏变异为你工作,这样你就不用了。

然而,我们写的这种方法是非常不灵活的。所有它可以做的是数组中的每个数字的两倍,但是我可以想象许多其他操作,我们可以做一个整数数组:三重所有的值,一半的所有值…的想法是无止境的。

我们来写一个通用的整数数组操纵器。我们将从一个Function界面开始; 这允许我们定义我们如何操纵每个整数。

然后,我们将编写一个map()同时使用整数数组和 a 的函数Function。对于数组中的每个整数,我们可以应用Function。

瞧!有了一些额外的代码,我们现在可以将任何整数数组映射到另一个整数数组。

我们可以进一步说明这个例子:为什么不使用泛型,以便我们可以将任何列表从一个类型转换到另一个类型?改变以前的代码不是那么难。

现在,我们可以映射任何List来List。例如,我们可以使用字符串列表并将其转换为每个字符串长度的列表。

我们map()被称为高阶函数,因为它将函数作为参数。能够传递和使用功能是一个强大的工具,因为它允许代码更加灵活。您可以编写这样的泛化函数,而不是编写重复的实例特定函数,map()这在很多情况下都是可重用的。

由于缺乏外部状态,除了更容易使用,纯功能也使组合功能变得更加容易。如果你知道一个函数是A -> B另一个函数B -> C,那么我们可以把这两个函数粘在一起来创建A -> C。

虽然您可以撰写不纯的功能,但通常会出现不必要的副作用,这意味着很难知道撰写功能是否正常工作。只有纯粹的功能才能保证编码人员的安全。

我们来看一个组合的例子。这是另一个常见的FP功能filter()。它可以让我们缩小列表中的项目。现在我们可以通过组合这两个函数来过滤我们的列表,然后再转换它。

我们现在有一些很小而强大的转换功能,并且通过让我们组合在一起,它们的功率大大增加。

功能编程比我在这里提供的功能还要多,但是这个崩溃课程已经足够了解“FRP”的“FP”部分。

功能反应编程

我们来看看函数式编程如何增加无效代码。

假设我们Switch,而不是提供一个Observable,而是提供自己的基于枚举的流Observable。

似乎没有办法我们现在可以使用Switch,LightBulb因为我们有不兼容的泛型。但是有一个明显的Observable模式Observable- 如果我们可以从一种流转换到另一种流?

记住map()我们刚刚在FP中看到的功能?它将一种类型的同步集合转换为另一种类型。如果我们可以将相同的想法应用于异步集合,Observable如何呢?

大达:这是map(),但是Observable。Observable.map()是所谓的操作员。运营商让您Observable以基本上任何可想象的方式转换流。

操作员的大理石图比以前看到的要复杂一些。我们来吧吧

顶行代表输入流:一系列彩色圆圈。
中间框代表运算符:将一个圆转换成一个正方形。
底线代表输出流:一系列彩色方块。
基本上,它是输入流中每个项目的1:1转换。

我们将其应用于我们的切换问题。我们从一开始Observable。然后我们使用,map()以便每次新State的发射它被转换成一个Boolean; 因此map()返回Observable。现在我们有正确的类型,我们可以构建我们的LightBulb。

好的,这很有用。但是这与纯功能有什么关系呢?你不能写内容map(),包括副作用?当然,你可以…但是,你使代码难以处理。此外,你错过了一些无效的操作员组合。

假设我们的State枚举有两个以上的州,但我们只关心完全开/关状态。在这种情况下,我们要过滤出任何中间状态。哦,看,filter()FRP 也有一个操作员; 我们可以用我们的方式map()来形成我们想要的结果。

如果将FRP代码与上一节所述的FP代码进行比较,您将看到它们是如何非常相似的。唯一的区别是FP代码正在处理同步集合,而FRP代码正在处理异步集合。

FRP中有大量运营商,涵盖了流程操作的许多常见情况,可以一起应用和组合。我们来看一个现实生活的例子。

我之前展示的Trello主屏幕非常简化 - 它只是有一个从数据库到UI的大箭头。但是真的,我们的主屏幕使用了一堆数据源。

特别是,我们有团队的来源,在每个团队的内部可以有多个板。我们要确保我们正在同步接收这些数据; 我们不希望有不匹配的数据,例如没有其父级团队的董事会。

为了解决这个问题,我们可以使用combineLatest()运算符,它需要多个流,并将它们组合成一个复合流。特别有用的是,它每次更新任何输入流时都会更新,因此我们可以确保我们发送到UI的打包数据是完整的和最新的。

FRP中真的有很多运营商。这里只是一些有用的…经常,当人们首先参与FRP,他们看到操作员列表和微弱。

然而,这些运营商的目标不是压倒一切 - 它是为了在应用程序中建模典型的数据流。他们是你的朋友,而不是你的敌人。

处理这个问题的建议是一次一步走。不要一次记住所有的操作员; 相反,只是意识到一个操作员可能已经存在你想要做的事情。在需要时查找它们,经过一些实践,你会习惯于他们。

带走
我努力回答“什么是功能反应式编程?我们现在有一个答案:它是反应流与功能运算符相结合。

但为什么要尝试FRP?

反应流允许您通过标准化组件之间的通信方法来编写模块代码。无功数据流也允许这些组件之间松动耦合。

反应流也本质上是异步的。也许你的工作是完全同步的,但我所工作的大多数应用程序取决于异步用户输入和并发操作。使用与异步代码设计的框架更容易,而不是尝试编写自己的并发解决方案。

FRP的功能部分是有用的,因为它为您提供了以合理的方式处理流的工具。功能运算符允许您控制流如何相互交互。它还为您提供了通用编程方面的常见逻辑重现工具。

功能反应式编程不直观。大多数人开始编写主动,不纯,自己包括在内。你做的足够长,开始巩固你的想法,主动,不纯的编码是唯一的解决方案。打破这种心态可以使您能够通过功能反应式编程编写更有效的代码。

资源
我想感谢/指出我为这次演讲所提供的一些资源。

cycle.js对主动与反应性代码有很好的解释,我从此大量借来了这个演讲。
埃里克·梅耶尔(Erik Meijer)对主动/反应对立性做了一个奇妙的演讲。我从这里借了功能的四个基本功效。这个谈话是很数学的,但如果你可以通过它,这是非常有启发性的。
如果您想更多地了解功能性编程,我建议您尝试使用实际的FP语言。由于Haskell严格遵守FP,Haskell尤为明显,这意味着您无法通过实际学习来欺骗您的出路。如果您想进一步调查,“学习Haskell”是一本很好的免费在线书籍。
如果您想了解更多有关FRP的信息,请查看我自己的一系列博客文章。这些帖子中涵盖的一些主题涵盖在这里,所以阅读这两个内容将会有点重复,但更多的细节在RxJava尤其如此。

原创粉丝点击