十一、设计函数式应用
按照函数式编程的规则创建一个完整的应用似乎是一项不可能完成的任务。如果你没有任何副作用,你怎么能写出有意义的软件呢?为了执行任何类型的计算,您至少需要一些输入和显示结果。
函数式语言有各种机制来规避这些限制。我们将很快介绍其中的一些,以便您能够更好地了解如何以纯功能的方式编写应用。
然后,我们将更深入地了解一种称为函数式反应式编程(FRP的范例,作为设计具有用户界面的应用的一种方式。我们将为使用 PHP 中的这个技术来判断是否有可能使用它来编写完整的应用奠定基础。
在本章中,您将了解以下主题:
- 用纯函数式语言编写完整的应用
- 函数反应式编程
- 使用 FRP 设计 PHP 应用
纯函数式应用的体系结构
应用与函数类似。如果您有一个没有任何输入的应用,其结果将始终是相同的。您可以修改源代码中的某些值并重新编译软件以更改其结果,但这与我们首先编写应用的主要原因相反。
这就是为什么您需要一种向应用提供数据的方法,以便它执行任何类型的有意义的计算。这些输入可以有多种类型:
- 命令行参数
- 文件内容
- 数据库内容
- 图形界面中的字段
- 第三方服务
- 网络请求
在所有这些中,只有第一个可以被认为没有破坏整个应用的引用透明度。如果将应用视为一个大功能,则可以将命令行上的数据作为参数来考虑,从而保持一切都是纯的。所有其他类型的输入实际上都是不纯的,因为随后的两次数据检索可能会导致不同的值。
解决这个问题的典型哈斯克尔方法是使用IO 单子。IO monad 将所有步骤存储在队列中,而不是立即执行其操作。如果您将此 IO 操作命名为main
,Haskell 将知道在执行编译程序时必须运行它。
显然,如果在 monad 中执行任何类型的 IO 操作,那么应用本身就不再是纯粹的。但是,代码本身可以以引用透明的方式编写。当 IO monad 运行时,Haskell 运行时将执行所有不纯净的操作,然后将传递各种获得的值。使用此技巧,您可以编写纯功能代码,并享受其带来的所有好处,同时仍然可以执行 IO 操作。
这种方法在 Haskell 中是可用的,因为您可以使用 monad 转换器来组合多个 monad。do 表示法也有很大帮助,它可以编写封装在 IO monad 中的代码,而不需要与之相关的所有开销。例如,下面是一个小程序,它读取终端中的行,并以相反的顺序打印这些行:
main = do
line <- getLine
if null line
then return ()
else do
putStrLn $ reverseWords line
main
reverseWords :: String -> String
reverseWords = unwords . map reverse . words
它读起来与执行相同任务的任何命令式源代码基本相同。PHP 缺少语法糖,也没有 monad transformers 的实现,因此很难做到这一点。这就是为什么我们要做出上一章讨论过的妥协,或者我们需要一些其他的方法,我们将在下一节中看到。
起作用的想法是可以概括的。任何不纯函数都可以分为两个函数,一个是纯函数,另一个是封装副作用和副作用的函数。这正是我们在上一章中提到的,当时我们指出,大多数不纯函数应该包含在 MVC 应用的控制器中。
如果您有一个不纯函数f
以A
为参数并返回B
,您可以创建以下两个函数:
- 纯函数
g
,接受A
并返回D
参数。参数D
是对需要执行的 IO 操作的描述。 - 一个不纯的函数
h
接受D
并像解释器一样执行所描述的操作。
如果我们以 Haskell 应用为例,Haskell 运行时本身就是我们不纯的h
函数。如果我们的源代码返回 IO monad 的实例,就像上面的示例所做的那样,它将被用作D
参数,并解释副作用。
如果您使用的是使用 TyfT22.SyfOff-Ty3x 框架的 Web 应用,我们可以将该框架视为不纯净的 To0Tyr 函数,而 OutT1 参数则是执行控制器的结果。另一种可能是在函数代码周围添加自定义的不纯包装。
其主要思想是将h
等函数的数量减少到最小。Haskell 强制您只有一个这样的函数,它甚至隐藏在运行时中。如果您使用的是 PHP,则由您来尽可能有效地执行此规则。
这种对计算进行描述并由解释器执行的概念是函数世界中许多更高级技术的核心。它在整个计算机编程中也相当重要。如果我们走一段距离,我们可以看到以下内容:
- 描述就像一个抽象语法树(AST)
- 解释器获取 AST 并运行它
这就是大多数现代编译器的工作方式,它们首先解析源代码以在 AST 中进行转换,然后对其进行解释以创建二进制文件。在大多数复杂的应用中,您还会一次又一次地发现相同的模式。
使用这种结构的一种高级构造是游离单子。这种单子是当前功能领域的一个热门话题,它的使用正在快速增长。我们缺少了很多理论来探讨这个话题,但是如果你感兴趣,你肯定会在互联网上找到很多信息,例如,http://underscore.io/blog/posts/2015/04/14/free-monads-are-simple.html 。
但是,当您在应用的生命周期中接受用户交互时,这种模式是有问题的。由于主要思想是通过描述来延迟有效计算的执行,因此不能执行部分计算以显示用户界面,然后对用户输入作出反应。这是 FRP 试图解决的问题之一。
从功能反应式动画到功能反应式编程
当涉及函数式编程时,通常情况下,手边的主题背后的基础可以追溯到一点。1997 年,科纳尔·艾略特(Conal Elliott)和保罗·胡达克(Paul Hudak)发表了一篇论文,名为功能性反应动画,或 Fran。
Fran 的主要目标是允许使用两个称为行为和事件的概念对动画进行建模。行为是基于当前时间的值,事件是基于外部或内部刺激的条件。这两个概念允许我们在任何时间点表示任何类型的动画,尽管动画本身是连续的。
您可以使用行为和事件来描述动画,而不是像通常那样直接创建动画的表示。然后将解释和表示留给底层实现。这与我们刚才描述的类似。由于诸如键盘输入或鼠标单击之类的事件可以在 Fran 内部编码,因此您正在创建的模型允许纯函数式应用响应这些外部输入。
反应式编程
在我们进一步讨论之前,让我们先谈谈反应性在编程世界中的含义。这是一个在过去几年中获得了相当大吸引力的想法。
首先是反应性宣言(http://www.reactivemanifesto.org/ ),它列出了所有软件都非常有趣的属性列表。这些特性是:响应能力、弹性、弹性和信息驱动。
维基百科(https://en.wikipedia.org/wiki/Reactive_programming 定义说明了一些完全不同的东西:
在计算领域,反应式编程是一种围绕数据流和变化传播的编程范式。这意味着应该可以用所使用的编程语言轻松地表示静态或动态数据流,并且底层执行模型将通过数据流自动传播更改。
然后给出表达式a=b+c的示例,其中a
的值在b
或c
中的任何一个发生变化时自动更新。
JavaScript 界对这一想法兴高采烈,有Bacon.js
或RxJS
等库。所有这些库共享的核心思想围绕事件或事件流。
正如我们所看到的,反应式编程有多种定义。可悲的是,没有一个和我们刚刚了解到的弗兰的情况完全相符。至少从七十年代开始,这个定义就一直在流传,我们将在本章剩余部分保留的定义是学术定义,可以在维基百科上找到。
我不是说其他的都是无效的,只是我们需要有一个共同点。另外,下次当你和某人谈论反应式编程时,首先要确保你对主题的理解是一致的。
作为反应式编程的最后一个例子,让我们考虑下面的代码:
<?php
$a = 10;
$b = 5;
$c = $a + $b;
echo $c;
// 15
$a = 23;
echo $c;
在传统的命令式语言中,最后一行仍然显示 15。但是,如果我们的应用遵循反应式编程设置的规则,$a
的新值也会影响$c
的值,程序将显示 28。
功能性反应式编程
正如您可能猜到的,当进行其他更改时,随着时间的推移而更改的值远远不是引用透明的。此外,一些函数式语言中完全没有变量的概念。我们如何协调反应式编程和函数式编程?
其核心思想是在函数需要时为其设置时间组件和 previous events 参数。这正是弗兰对行为和事件的建议。时间和事件通常都建议作为流使用。使用函数映射和过滤,您可以决定您感兴趣的流上的哪些事件。
您的函数从该流中获取一个或多个输入以及应用的当前状态。然后,它们必须返回应用的新状态。运行时将负责在事件发生时调用各种已注册函数。
您可能会觉得它类似于事件驱动编程。在某种程度上是这样,但有很大的区别。在传统的事件驱动应用中,会触发事件,但处理程序的返回值通常并不重要;他们需要有副作用来完成一些事情。
执行 FRP 时,运行库负责编排所有已注册的处理程序。保持应用的当前状态,将其传递给每个处理程序,并使用其结果更新它。这允许函数是纯函数。
另一种可能比事件驱动编程更接近的编程范式是 actor 模型。我不会在这里描述它,因为它超出了本书的范围,但对于了解它的人,我只想说有两个主要区别:
- 由于您有纯函数而不是参与者,因此您不能让私有状态影响您对给定消息或事件的响应方式
- 运行时管理事件流;处理程序无法向应用的其他部分发送新消息
时间旅行
玻璃钢还有另一个好处。如果记录导致特定应用状态的事件序列,则可以重播它们。更好的地方是您可以实现所谓的时间旅行调试器。由于您的应用使用纯函数,因此您可以返回到任何时间点,并获得与以前完全相同的状态。
这种调试器还允许您来回回放任意数量的步骤,直到您能够准确地确定发生了什么。此外,您还可以对代码进行更改并播放相同的事件,以查看修改对软件的影响。
如果你想看到这样一个调试器在运行,你可以使用Elm语言提出的调试器,特别是他们的在线版本,它是一个简单的马里奥平台游戏(的实现)http://debug.elm-lang.org/edit/Mario.elm )。
Elm 调试器可能是此类调试器中的第一个。虽然类似的想法已经在传统语言中实现,但命令式编程的本质要求我们记录的不仅仅是事件流。这就是为什么这是一个非常昂贵的操作,大大降低了程序的执行速度。
您还需要从头重新启动程序,以确保达到相同的状态。然而,在纯应用中,您可以用更直接的方式来实现这一点。现在正在创建更类似于 Elm 中的实现,例如,为ReactJavaScript 库创建。
免责声明
有 FRP 和 FRP,但与其解释这个想法的创造者,不如让我引用他的话:
在过去的几年中,关于 FRP 的一些东西引起了程序员的极大兴趣,激发了几个用各种编程语言实现的所谓“FRP”系统。然而,大多数此类系统都缺乏 FRP 的基本特性。
您可以在 GitHub(上)看到全文以及相关幻灯片和视频 https://github.com/conal/talk-2015-essence-and-origins-of-frp )。
通常,学术界和人们对研究结果的使用之间存在某种分歧。我不会详细讨论这些细节,因为这应该只是一个介绍性的章节。然而,你必须意识到这一事实。
争论的主要焦点是 FRP 是关于时间 T0 连续时间 T1 的事实,而大多数实现仅考虑了 T2 T2 离散事件或值 Ty3 T3。如果您想了解更多关于这些差异的信息,我强烈建议您观看之前链接的视频,该视频位于 Fran 和 FRP 的创建者 Elliot Conal 的 GitHub 存储库中。
更进一步
关于函数式反应式编程还有很多其他的事情要说。事实上,整本书都致力于这一主题。然而,这只是一个介绍,所以我们就到此为止。如果你想要一种不依赖于特定语言的通用方法,我可以推荐斯蒂芬·布莱克希思和安东尼·琼斯最新出版的函数式反应式编程。
在实现方面,ReactiveX项目试图联合多个项目上可用的库。您可以在官方网站上找到更多信息 http://reactivex.io/ 。在编写本报告时,涵盖了以下语言:;java,SWIFT,Python,PHP,Scala,JavaScript,Ruby,Culjule,Rug,Go,C++,C++,Lua。
正如之前的免责声明和 ReactiveX 网站的介绍所述,目前 FRP 的学术概念与 FRA 原稿的延伸以及当今程序员所指的术语相混淆。前面提到的书和 ReactiveX 库都谈到后者,而不是原始含义。这并不意味着这些都是坏主意,恰恰相反;只是它不是真的。
ReactiveX 底漆
Rx*
库选择通过将经典观察者模式扩展到Observable
模型来实现功能反应范式。对于由Observable
模型实例表示的给定值流,您最多可以定义三个不同的处理程序:
- 每次有新值可用时,都会调用
onNext
处理程序 - 当出现异常时,
onError
处理程序将被调用 - 当流关闭时,
onCompleted
处理程序将被调用
这种方法可以轻松处理多个异步事件,而无需编写复杂的样板代码来管理它们之间的依赖关系。与传统的观察者模式相反,添加了向流结束和错误发送信号的能力,以使接口与 iterables 相协调。
ReactiveX 还定义了一组操作符来操纵可观测值及其值。有助手方法可以创建各种类型的流,从范围到数组,通过无限重复的值和定时释放事件传递。
您还可以通过将函数映射到每个发出的值,将它们分组到新的可观察值或值数组中来操纵流本身。您还可以过滤这些值,跳过或获取一定数量的值,在一定时间内限制排放量,并抑制重复。
文件(http://reactivex.io/documentation/operators.html 有一个完整的可用操作列表,以及一个很好的决策树,根据上下文决定使用哪种操作。
RxPp
在我们开始研究 RxPHP 的一些示例之前,我想指出,Packt Publishing 还出版了一本完整的书,PHP 反应式编程,关于这个主题。您可以在他们的网站上找到更多信息 https://www.packtpub.com/web-development/php-reactive-programming 。这就是为什么我们将只探讨一些基本示例,让您了解如何使用该库。如果你对这个主题感兴趣,我强烈建议你阅读这本专门的书。
在对 ReactiveX 进行了非常简短的介绍之后,让我们看看如何使用它。首先,我们需要安装所需的库。我们将在 ReachHP 的流库周围使用一个小包装器,使其可用于 RxPHP,以便演示如何访问磁盘上的文件。以下composer
调用应安装所有需要的依赖项:
composer require rx/stream
现在已经安装了库,您可以解析来自任何 PHP 流的数据。例如,CSV 文件:
<?php
use \Rx\React\FromFileObservable;
use \Rx\Observer\CallbackObserver;
$data = new FromFileObservable("11-example.csv");
$data = $data
->cut()
->map('str_getcsv')
->map(function (array $row) { return $row; });
$data->subscribe(new CallbackObserver(
function ($data) { echo $data[0]."\n"; },
function ($e) { echo "error\n"; },
function () { echo "done\n"; }
));
我们首先为要读取的文件创建一个可观察的流,然后应用一些转换:按行分隔输入,解析数组中的 CSV 字符串,并应用可能需要的任何其他数据处理。正如您可以从我们将结果重新分配给$data
变量这一事实中推断的那样,操作没有执行到位,但每次都会返回一个新实例。
然后,我们可以向流订阅处理程序。在本例中,我们只需打印每个元素的第一行。不是很实用,但对于一个小例子来说足够有效。
如果您使用的是PostgreSQL,则存在一个允许您使用 Rx 访问数据库的包。您可以使用它使用流检索数据。您可以使用composer
调用进行安装:
composer require voryx/pgasync
创建查询相当容易。这是一个使用连接凭据创建客户机的问题,然后调用其中一个方法来创建一个可观察的实例,您可以订阅该实例:
<?php
$client = new PgAsync\Client([ "user" => "user", "database" => "db" ]);
$client->query('SELECT * FROM my_table')->subscribe(new CallbackObserver(
function ($row) { },
function ($e) { },
function () { }
));
这里是最后一个示例,演示了 Rx 在流本身上提供的一些更高级的过滤和转换可能性。在运行之前,请尝试猜测输出将是什么:
<?php
use \React\EventLoop\StreamSelectLoop;
use \Rx\Observable;
use \Rx\Scheduler\EventLoopScheduler;
// Those are needed in order to create a timed interval
$loop = new StreamSelectLoop();
$scheduler = new EventLoopScheduler($loop);
// This will emit an infinite sequence of growing integer every 50ms.
$source = Observable::interval(50, $scheduler);
$first = $source
->throttle(150, $scheduler) // do not emit more than one item per 150ms
->filter(function($i) { return $i % 2 == 0; }) // keep only odd numbers
->bufferWithCount(3) // buffer 3 items together before emitting them
->take(3); // take the 10 first items only
$second = $source
->throttle(150, $scheduler)
->take(10);
$first->merge($second) // merge both observable
->subscribe(new CallbackObserver(
function ($i) { var_dump($i); },
function ($e) { },
function () { }
));
$loop->run();
如果您试图运行最后一段代码,则需要 RxPHP 的开发版本,因为throttle
是最近才实现的。如果您的最小稳定性参数设置为dev
版本,您可以使用以下方式安装:
composer require reactivex/rxphp:dev-master
实现参考透明度
如示例所示,创建流并订阅流相当简单。我们还可以很容易地想象我们如何以一种允许在多个可观察实例之间重用的方式分解处理程序。
然而,Rx 并没有为我们解决的问题是,需要应用架构来尽可能实现引用透明性。仅仅创建一个新的数据库查询作为一个可观察对象是不够的。
我能给你的建议和你在上一章中听到的一样,就是试着把所有不纯净的代码分离在一个地方。在我们的例子中,这可以通过在一个唯一的文件中创建所有流来实现,例如,您的index.php
文件,并在其他地方声明处理程序。
可以单独测试各种处理程序,您可以快速建立对它们的信心,因为它们是引用透明的。然后,集成和功能测试将负责测试流本身和整个应用。
如果您试图在现有框架中使用 Rx,您可以在控制器中声明流,并以与前面所述相同的方式分离处理程序。
总结
功能性反应式编程允许我们协调纯功能和事件管理。这意味着可以创建需要用户输入或访问第三方服务和外部数据源的应用。这一点尤其重要,因为越来越多的网站利用 web 套接字和其他此类技术不断向用户推送数据。
除了访问数据源,FRP 在进行用户界面工作时也非常有用。任务通常在 Web 上使用 JavaScript 完成,因为 PHP 主要用于处理请求本身并提供 HTML 响应。然而,PHP 可能会更多地在桌面上使用,比如 PHP7(的 beta 版中提供的围绕libui的包装器 https://github.com/krakjoe/ui )。
PHP 中的桌面应用在社区中是一个相当新的主题,现在可能是基于最先进的函数式反应式编程创建一些最佳实践的好时机。
我们只是简单介绍了这种设计应用的新方法,因为它需要比一章多得多的内容才能完全做到这一点。如果你想更多地了解这个话题,前面提到的两本书都是一个很好的起点。
在本章中,我们了解了一些 FRP 的历史。我们还试图发现传统的反应式编程和功能性编程之间的差异。我们很快谈到了时间旅行调试,然后展示了几个 PHP 示例。
你刚刚读完这本书的最后一章。我希望你读这本书和我写这本书一样开心。我也希望我能让你对函数式编程感兴趣,并且你能在未来的项目中尝试实现我们在本书中看到的各种技术。对我来说,最好的回报莫过于知道我能够让一位开发伙伴对这个精彩的主题感兴趣。
在我们分开之前,我建议您先阅读一下Appendix
、当我们谈论函数式编程时,我们在谈论什么。它包含了对函数式编程是什么、它的好处和它的历史的更全面的定义。您还可以在末尾找到一个词汇表,解释各种术语,其中一些在本书中看到,另一些是新的。
再见,谢谢你的鱼。
版权属于:月萌API www.moonapi.com,转载请注明出处