在函数式编程中,可组合性意味着什么?
当function程序员说某个东西是可组合或不可组合时,它们是什么意思?
我读到的这种types的一些陈述是:
- 控制结构不可组合。
- 线程不构成。
- 一元操作是可组合的。
Marcelo Cantos给出了一个很好的解释 ,但是我认为可以稍微精确些。
当一些事件可以以某种方式组合以产生相同types的事物时,一种事物是可组合的。
控制结构的可组合性。 像C这样的语言对expression式进行了区分,这些expression式可以用操作符来组合,以产生新的expression式,也可以使用控制结构(如if
, for
和“顺序控制结构”来简单地执行语句。 关于这种安排的事情是,这两个类别不是平等的 – 许多控制结构使用expression式(例如通过if
来评估expression式来select要执行哪个分支),但是expression式不能使用控制结构(例如,你不能返回一个for
循环)。 尽pipe想要“返回循环”看起来可能是疯狂的或无意义的,事实上,将控制结构视为可以存储和传递的第一类对象的一般概念不仅是可能的,而且是有用的。 在像Haskell这样的懒惰的函数式语言中,像if
和for
这样的控制结构可以表示为普通函数,它可以像任何其他术语一样在expression式中进行操作,从而可以根据传递的参数“构build”其他函数,并将它们返回给调用者。 所以,当C(例如)将“程序员可能想要做的事情”分成两类,并且限制这些类别中的对象可以被合并的方式时,Haskell(例如)只有一个类别,并且没有这样的限制所以在这个意义上它提供了更多的可组合性。
线程可组合性。 我会假设马塞洛·坎托斯(Marcelo Cantos)所说的,你确实在谈论使用锁/互斥锁的线程的可组合性。 这是一个稍微棘手的情况,因为面对它,我们可以使用多个锁的线程; 但重要的一点是,我们不能让线程使用多个锁,并保证它们有意 。
我们可以将locking义为一种具有某些可以在其上执行的操作的事物,这些操作具有一定的保证。 一个保证是:假设有一个锁对象x
,然后假定每个调用lock(x)
进程最终调用unlock(x)
,任何对lock(x)
调用最终都会成功地返回, x
被当前线程/进程locking。 这种保证大大简化了对程序行为的推理。
不幸的是,如果世界上有一个以上的锁,就不再是这样了。 如果线程A调用lock(x); lock(y);
lock(x); lock(y);
线程B调用lock(y); lock(x);
lock(y); lock(x);
那么有可能A抓住锁x
和B抓住锁y
,它们将无限期地等待另一个线程释放另一个锁:死锁。 所以,锁不是可组合的,因为当你使用多个锁时,你不能简单地声称重要的保证仍然成立 – 而不是没有详细分析代码来看它如何pipe理锁 。 换句话说,你不能再把function当作“黑匣子”了。
可组合的东西是好的,因为它们使抽象成为可能 ,这意味着它们使我们能够推理代码而不必关心所有的细节,并且减less了程序员的认知负担。
Linux命令行的一个简单例子就是Linux命令行,其中pipe道字符可以让您以几乎无限的方式组合简单的命令(ls,grep,cat等等),从而“组合”大量的复杂行为less数简单的原语。
可组合性有几个好处:
- 更一致的行为:例如,通过一个单一的命令实现“一次显示一个页面”(
more
),你可以获得一定程度的分页一致性,如果每个命令都要实现它们自己的机制,命令行标志)做分页。 - 减less重复的实现工作(DRY):不是有很多不同的分页实现,而是只有一个在任何地方都可以使用。
- 为给定量的实现努力提供了更多的function:现有的原语可以组合起来,解决更大范围的任务,而不是相同的努力去实现单一的,不可组合的命令。
正如Linux命令行示例所示,可组合性不一定局限于函数式编程,但其概念是相同的:只有less量代码可以执行受限任务,并通过适当地路由输出和input来构build更复杂的function。
关键在于函数式编程非常适合这样的情况:使用不可变variables和副作用限制,您可以更轻松地编写代码,因为您不必担心被调用的函数内部会发生什么情况 – 比如更新共享variables,以便结果将对某些操作序列无效,或者访问共享锁,所以某些调用序列会导致死锁。
这是函数式编程的可组合性 – 任何函数都只依赖于它的input参数,输出可以传递给任何可以处理返回值types的函数。
通过扩展,数据types越less,组合性越好。 Clojure的Rich Hickey说了一些话
每一个新的对象types都与先前所有的代码本质上是不兼容的
这当然是一个很好的点。
实际的可组合性也依赖于一小部分数据types的标准化,就像Unix命令使用“制表符分隔的基于行的文本”标准一样。
后记
Eric Raymond写了一本关于Unix哲学的书,他列出的两个devise原则是
- 模块化规则:用干净的界面写简单的部分。
- 构图规则:devise程序与其他程序连接。
从http://catb.org/~esr/writings/taoup/html/ch01s06.html#id2877537
函数式编程中的可组合性可以说是将这些原则降低到了单个函数的水平。
计算机科学的构成是通过聚合更简单的行为来组装复杂行为的能力。 function分解就是这样一个例子,一个复杂的function被分解成更小的易于掌握的function,并通过一个顶层函数组装到最终的系统中。 顶层的function可以说是把整件作品“组合”起来了。
某些概念不容易构成。 例如,一个线程安全的数据结构可以允许安全地插入和移除元素,并且通过locking数据结构或者它的一些子集来实现这个function,以便一个线程可以执行必要的操作,而不需要改变它们的变化,数据结构损坏 – 而它的工作。 但是,业务function可能需要从一个集合中删除一个元素,然后将其插入到另一个集合中,并且使整个操作以primefaces方式执行。 问题是locking只发生在每个数据结构中。 您可以安全地从一个元素中删除元素,但是您可能会发现由于某些关键违规而无法将其插入到另一个元素中。 或者你也可以尝试将它插入到第一个中,然后从第一个中删除它,结果发现另一个线程从你鼻子下面偷了它。 在意识到自己无法完成手术的时候,你可以试着把事情做回原样,只是发现由于类似的原因,逆转失败了,你现在处于陷阱! 当然,您可以实现一个涵盖多个数据结构的更丰富的lockingscheme,但只有在每个人都同意新的lockingscheme的情况下,才会有效,并且始终承担使用它的负担,即使它们的所有操作都在单个数据结构。
互斥体式locking因此是不构成的概念。 您不能简单地通过聚合较低级别的线程安全操作来实现更高级别的线程安全行为。 在这种情况下,解决scheme是使用一个构成的概念,如STM 。
我同意Marcelo Cantos的回答,但是我认为它可能会比一些读者有更多的背景,这也与为什么函数式编程中的组合是特殊的有关。 函数式编程函数的组成与math中的函数组成基本相同。 在math中,你可能有一个函数f(x) = x^2
,函数g(x) = x + 1
。 编写函数意味着创build一个新函数,其中函数参数赋予“内部”函数,“内部”函数的输出作为“外部”函数的input。 用g
内部构成f
外部可以写成f(g(x))
。 如果为x
提供1的值,则g(1) == 1 + 1 == 2
,所以f(g(1)) == f(2) == 2^2 == 4
。 更一般地, f(g(x)) == f(x + 1) == (x+1)^2
。 我使用f(g(x))
语法描述了组合,但是math家通常更喜欢不同的语法(f . g)(x)
。 我认为这是因为它更清楚地表明f composed with g
是一个单独的函数。
function程序完全使用组合原语构build。 Haskell中的一个程序可能过于简单化了一个以运行时环境为参数的函数,并返回对该环境某些操作的结果。 (Grokking这个陈述将需要一些单子的理解。)其他的一切都是用math意义上的成分来完成的。
另一个例子:在.NET中考虑asynchronous编程。 如果您使用C#这样的语言,需要通过Begin / End API进行一系列asynchronous(非阻塞)I / O调用,那么为了依次调用两个操作Foo
和Bar
,必须将两个操作方法( BeginFooAndBar
, EndFooAndBar
),其中BeginFooAndBar
调用BeginFoo
并将callback传递给Intermediate
,然后Intermediate
调用BeginBar
,并且必须通过中间值和IAsyncResult
状态信息进行处理,如果你想要一个try
– catch
块事情,那么祝你好运,你需要在三个地方复制exception处理代码,这很糟糕。
但是用F#,还有async
types,构build在可组合的function延续之上,所以你可以编写例如
let AsyncFooAndBar() = async { let! x = Async.FromBeginEnd(BeginFoo, EndFoo) let! y = Async.FromBeginEnd(BeginBar, EndBar, x) return y * 2 }
或者你有什么,这很简单,如果你想try
– 很好,代码是在一个方法,而不是三个分散,你只是try
– 围绕它,它的工作原理。
这是一个真实世界的例子。 住在你家里的所有人的名字都是你家里所有男性的名字,以及你家里所有女性的名单。
这是可组合的,因为这两个子问题中的每一个都可以独立解决,而不会干涉另一个。
另一方面,许多食谱是不可组合的,因为步骤必须以特定的顺序完成并且依赖于其他步骤的结果。 你必须打破鸡蛋之前,你拂!
可组合性允许开发者社区不断提高抽象级别,多级别,而不被链接到基础层。