作者:Julian Matschinske

基于流的编程中的数据与自然界中可观察到的流有很多共同之处

如果你有编码经验,你肯定熟悉命令式编程范式,即使你可能没有意识到它。也许您还听说过其他编程范例,例如函数式编程。但究竟什么是编程范式?为什么我们如此关心基于流的编程范式?

如果您是经验丰富的编码人员,则可能需要立即跳到基于流的部分。但我认为仍然值得阅读其他部分,以明确差异并变得清晰。

抽象化

在开始之前,我们想考虑一件小事:编程语言的目的是什么?这似乎是一个微不足道的问题,但实际上并非如此。最重要的目的是允许人类编写最终必须在计算机上执行的程序。但这显然不能是唯一的目的,因为像这样的汇编代码会这样做:

mov     edx,len
mov     ecx,msg
mov     ebx,1
mov     eax,4
int     0x80
mov     eax,1
int     0x80
section     .data
msg     db  'Hello, world!',0xa
len     equ $ - msg

这是着名的"hello world"程序,除了将"hello world"打印到屏幕上之外什么都不做。

在计算机编程的开始,你实际上必须像这样编写代码。但正如您可以想象的那样,程序很快就会变得非常混乱。今天的软件系统如果代码看起来像那样,就永远无法编写或维护。当今软件系统的复杂性和功能只能通过一种出色的方法来实现,这种方法是编程语言发展的特点:抽象。

抽象是隐藏不需要的细节的概念。我们每个人都使用抽象来理解和固定我们周围的世界。当我们看到一棵树时,我们对它到底有多少叶子不感兴趣。当我们看到一片森林时,我们对它看到了多少棵树或它们到底是什么树不感兴趣。当我们看到美丽的风景时,森林可能只是其中的一部分,背景是湖泊和令人印象深刻的山峰。

编程中的抽象非常相似。当你看到上面的代码时,你似乎需要多少行才能完成一项琐碎的任务,比如将"hello world"打印到屏幕上—— 事实也确实如此。出于这个原因,更高级别的编程语言试图隐藏这些细节,并提供更简单的说明,例如给你做这项工作。也许您无法详细了解它的工作原理,但是您需要知道吗?print

因此,这是编程语言的真正任务——隐藏实现的细节,这样你就可以专注于重要的事情,你的程序的逻辑,以缩小计算机语言和人类思维之间的差距。

命令式编程

让我们从命令式编程的例子开始。这种范式的著名代表是Java,Python以及您可能想到的大多数其他语言。这些语言或多或少地直接告诉您的计算机要做什么以及以什么顺序。

a = 5
b = 3
c = a + b
print c

很明显这里发生了什么,不是吗?首先,我们分配到 .然后到 .然后,我们添加并存储在 .最后,我们打印到标准输出。5a3babcc

当然,这看起来微不足道,确实如此。我们在这里要注意的是,我们可以直接读取这些指令的执行顺序以及它们的作用。更重要的是:毕竟存在明确的指令。不是我们在每个范式中都有的东西,正如您很快就会看到的那样。

我们刚刚研究了命令式编程的一个重要部分:指令的排序。

另一个重要部分是所谓的无条件和有条件跳跃。它们允许有条件地执行和重复代码序列。您可能知道它们是循环或循环。whilefor

while a != b:
    if a > b:
        a = a - b
    else:
        b = b - a
return a

这是欧几里得的算法,计算两个整数的最大共分数(GCD)。

当您将这些命令式代码示例与上一节中的汇编程序代码进行比较时,您会发现它们更易于阅读和理解。但两者实际上都是命令式编程的例子。为什么?最后,代码必须在计算机硬件上执行。该硬件最像所谓的冯-诺依曼计算机,它以某种方式工作。简单地说,它有一个由数十亿个单细胞和一个处理单元组成的全局内存。当我们为变量赋值时,冯-诺依曼计算机会将此值写入存储单元,并记住与变量链接的记忆单元。添加两个变量时,计算机必须查找这些单元格中的变量值,计算总和,然后将其写回另一个单元格中。

所有命令式语言都像这样工作,你写的所有东西(即使它非常抽象)都可以映射到这种指令集上。命令式范式之所以如此受欢迎,是因为我们的计算机硬件就是这样工作的,因为我们可以很容易地以这种方式控制它。

函数式编程

我们要研究的下一个范例是函数式编程范式。你可能会想"嘿,我已经知道了Python的函数。但这与我们谈论的功能并不完全相同。实际上,我们不想在这里深入探讨函数式编程,但是在研究基于流的范例之前,我们想向您展示编程范例的另一个示例。

我们感兴趣的函数是数学意义上的函数。它们不能像读取或写入内存中的全局变量那样更改程序的状态。他们所能做的就是获取我们给他们的值并返回一个值。听起来很无聊,但真正使它们强大(同时令人困惑)的是将函数视为值的可能性。除此之外,这种功能还具有很大的优点,例如没有意外的副作用和不可预测的行为。

"函数怎么可能是值?"你可能会想。但究竟什么是函数呢?它是从一个值到另一个值的映射 - 就像地图一样!地图是...是的,一段数据,因此可以被视为值。一个例子是函数。它采用一个值列表,并将一个函数应用于该列表的每个元素。map

int_map :: (Int -> Int) -> [Int] -> [Int]

这是函数签名的 Haskell 表示法。签名不是函数的实现,而是它所操作的变量的类型。在这种情况下,采用一个将整数映射到整数和整数列表的函数,并返回另一个整数列表。int_mapInt -> Int[Int][Int]

我们不会查看map的实现,因为它对于理解功能范式并不是绝对必要的。如果您有兴趣,请看这里。

下面是 Haskell 中排序算法的示例:

qsort []     = []
qsort (x:xs) = qsort small ++ (x : qsort large)
  where
    small = [y | y<-xs, y<=x]
    large = [y | y<-xs, y>x]

非常令人印象深刻,不是吗?该算法是一种成熟的高效快速排序算法。这里重要的观察结果是,您无法直接看到指令,更不用说将在计算机上执行的订单了。函数式编程中的语句顺序与机器上的执行顺序不同。它仍然有效,并且比其命令式版本更容易阅读(老实说)。

基于流的编程 (FBP)

现在你已经等了这么久,我们终于想向您介绍基于流的编程。我们认为,这种范式可以用于许多仍然由命令式语言主导的场景中。基于流的范式试图以一种自然的方式抽象逻辑,并以一种明显的方式可视化其每个元素。

与命令式编程不同,FBP 不在全局内存上运行。在这方面,它与函数式编程非常相似。但与函数式编程不同,其元素的顺序很重要,并且有意义。

让我们看一个简单的例子:

FBP 中的算术表达式

这个FBP程序取三个数字,并计算算术表达式。我所说的元素是你可以看到的盒子以及它们之间的联系。abc(a+b)*c

从现在开始,我们将称这些盒子为操作员,因为它们就像处理数据的小操作单元。他们从输入中获取数据,并向输出发出新数据。 并且有两个输入和一个输出 - 这是有道理的,不是吗?+*

在这种情况下,我们还没有真正任何类型的控制流。所有数据每次都采用相同的路径。那么,我们如何才能实现与命令式编程相同的控制水平呢?

叉子操作员

为此,我们需要一种特殊的运算符 —— 我们称之为叉子运算符。

叉子操作员

它有两个输入和两个输出。在上面的示例中,输入为紫色和蓝色,输出为绿色和红色。你可以把它想象成一个开关,紫色是信号员。无论数据到达蓝色,都将被发送到绿色或红色。紫色是所谓的布尔值,要么是真的,要么是的。根据到达紫色的数据是真的(左图)还是假的(右图),到达蓝色的数据分别发送到绿色或红色输出。

我们甚至可以用类似的(但稍微复杂一些)的方法表示循环,但我们不想在这里显示整个FB范例的所有细节。相反,我们想给您一个印象,通过这种范式可以实现什么。

更高级别的FBP

我们刚刚看到了FBP的最低水平。您可以将其视为汇编程序的FBP版本。所以现在我们想看看一些更高层次的程序。你当然还记得我之前解释过的抽象概念。FBP 中的抽象是通过将多个运算符打包到一个新运算符中来实现的。你得到的是一个更强大的逻辑,你可以在你的程序中使用或与他人共享。对运算符的抽象越多,就越接近程序的软件体系结构视图。没错!您无需拿起铅笔绘制软件架构即可了解程序的工作原理 - 它已经存在!

让我们看一下这个例子:

这是一个网络服务器,允许用户在zip存档中上传图像,并将它们全部转换为黑白图片。之后,它们将再次打包并作为下载发送回用户。

想象一下,这是一个势在必行的程序...我相信你同意上面的图像是可以理解的,即使对于没有经验的程序员也是如此。

关于它的很酷的事情是,每个组件也可以在其他上下文中使用。对于命令式编程中的函数,情况并非总是如此:它们通常依赖于特定的全局内存状态或依赖于特定的环境。FBP运营商是完全独立的。


转自:https://bitspark.de/blog/what-the-hell-is-flow-based-programming