当功能程序员说某个东西可以组合或不可组合时,他们的意思是什么?
我读过的一些这类陈述是:
答案 0 :(得分:52)
Marcelo Cantos gave a pretty good explanation,但我认为可以稍微更准确一些。
当一些事物可以以某种方式组合以产生相同类型的事物时,一种事物是可组合的。
控制结构可组合性。像C这样的语言区分表达式,可以使用运算符来组合生成新表达式,语句 ,可以使用像if
,for
这样的控制结构和简单按顺序执行语句的“序列控制结构”来组合。关于这种安排的事情是这两个类别不是平等的 - 许多控制结构使用表达式(例如if
评估的表达式来选择执行哪个分支),但表达式不能使用控制结构(例如,您不能返回for
循环)。虽然想要“返回for
循环”似乎是疯狂或毫无意义的,但实际上将控制结构视为可以存储和传递的第一类对象的一般想法不仅可行而且有用。在像Haskell这样的惰性函数语言中,像if
和for
这样的控制结构可以表示为普通函数,它可以像任何其他术语一样在表达式中进行操作,从而实现“构建”其他函数的功能。根据传递的参数进行操作,并将它们返回给调用者。因此,虽然C(例如)将“程序员可能想要做的事情”划分为两个类别并限制了这些类别中的对象可以组合的方式,但Haskell(例如)只有一个类别并且没有强加这样的限制,从这个意义上说,它提供了更多的可组合性。
线程可组合性。我认为正如Marcelo Cantos所做的那样,你真的在谈论使用锁/互斥锁的线程的可组合性。这是一个稍微棘手的情况,因为从表面上看,我们可以拥有使用多个锁的线程;但重要的一点是,我们不能让使用多个锁的线程保证它们具有。
我们可以将锁定义为一种具有某些可以在其上执行的操作的东西,它具有某些保证。一个保证是:假设有一个锁定对象x
,然后假设每个调用lock(x)
的进程最终调用unlock(x)
,对lock(x)
的任何调用最终将成功返回x
{1}}被当前线程/进程锁定。这种保证极大地简化了程序行为的推理。
不幸的是,如果世界上有多个锁,那就不再是真的了。如果线程A调用lock(x); lock(y);
并且线程B调用lock(y); lock(x);
那么A抓取锁x
并且B抓住锁y
并且它们将无限期地等待另一个线程到释放另一个锁:死锁。因此,锁是不可组合的,因为当您使用多个锁时,您不能简单地声称该重要保证仍然存在 - 并非没有详细分析代码以查看它如何管理锁。换句话说,您再也不能将功能视为“黑匣子”。
可组合的东西很好,因为它们启用了抽象,这意味着它们使我们能够在不必关心所有细节的情况下推理代码,并且减少了程序员的认知负担。 / p>
答案 1 :(得分:32)
可组合性的一个简单示例是Linux命令行,其中管道字符允许您以几乎无限多种方式组合简单命令(ls,grep,cat,more等),从而“组合”大量复杂来自少数简单原语的行为。
可组合性有几个好处:
more
)你得到一定程度的
寻呼均匀性不会
如果每个命令都可以
实施自己的机制(和
命令行标志)进行分页。正如Linux命令行示例所示,可组合性不一定仅限于函数式编程,但概念是相同的:具有限制任务的小块代码,并通过适当地路由输出和输入来构建更复杂的功能。
关键是函数式编程非常适合这种情况:使用不可变变量和副作用限制,您可以更轻松地编写,因为您不必担心被调用函数的内幕发生了什么 - 比如更新共享变量因此,对于某些操作序列或访问共享锁,结果将无效,因此某些调用序列将导致死锁。
这是函数式编程可组合性 - 任何函数仅依赖于其输入参数,并且输出可以传递给任何可以处理返回值类型的函数。
通过扩展,具有更少的数据类型可提供更多可组合性。
中的Clojure的Rich Hickey说了些什么每个新的对象类型都是固有的 与所有代码不兼容 写入
这当然是一个很好的观点。
实用可组合性还取决于对一小组数据类型的标准化,例如Unix命令使用“制表符分隔的基于行的文本”标准。
<强>后记强>
Eric Raymond写了一本关于Unix哲学的书,他列出的两个设计原则是来自http://catb.org/~esr/writings/taoup/html/ch01s06.html#id2877537
功能性编程中的可组合性可以说将这些原则降低到个别功能的水平。
答案 2 :(得分:22)
计算机科学中的组合是通过聚合更简单的行为来组装复杂行为的能力。功能分解就是这样的一个例子,其中复杂的功能被分解成更小的易于掌握的功能,并通过顶级功能组装到最终系统中。顶级功能可以说是将这些部分“组合”成整体。
某些概念不易构成。例如,线程安全的数据结构可以允许安全地插入和删除元素,并且它通过锁定数据结构或其某个子集来实现这一点,这样一个线程就可以执行必要的操作而不会对其进行任何改动 - 并且数据结构已损坏 - 虽然它有效。但是,业务功能可能需要从一个集合中删除一个元素,然后将其插入到另一个集合中,并且需要以原子方式执行整个操作。问题是每个数据结构只发生锁定。您可以安全地从一个元素中删除元素,但是您可能会发现由于某些键违规而无法将其插入另一个元素中。或者你可以尝试将它插入一秒钟,然后从第一个中删除它,却发现另一个线程从你的鼻子下面偷了它。在意识到你无法完成操作之后,你可以试着把它们放回原来的样子,但却发现逆转因为类似的原因而失败了,你现在处于不确定状态!当然,您可以实现更丰富的锁定方案,该方案涵盖多个数据结构,但只有在每个人都同意新的锁定方案时才有效,并且即使所有操作都在一个单独的操作上,也要承担使用它的负担。数据结构。
因此,互斥式锁定是一个不构成的概念。仅通过聚合较低级别的线程安全操作,您无法实现更高级别的线程安全行为。在这种情况下,解决方案是使用构成的概念,例如STM。答案 3 :(得分:5)
我同意Marcelo Cantos的回答,但我认为它可能比某些读者有更多的背景,这也与函数式编程中的组合特殊性有关。函数式编程函数组成与数学中的函数组成基本相同。在数学中,您可能有一个函数f(x) = x^2
和一个函数g(x) = x + 1
。组合函数意味着创建一个新函数,其中函数参数赋予“内部”函数,“内部”函数的输出作为“外部”函数的输入。撰写f
外部g
内部可以写成f(g(x))
。如果您为1
提供x
的值,则为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))
语法描述了合成,但数学家通常更喜欢不同的语法(f . g)(x)
。我认为这是因为它更清楚f composed with g
本身就是一个函数,只需要一个参数。
功能程序完全使用组合原语构建。 Haskell中的程序可能过于简单化了一个函数,该函数将运行时环境作为参数,并返回对该环境进行某些操作的结果。 (Grokking这个陈述需要对monad有所了解。)其他所有内容都是用composition in the mathematical sense完成的。
答案 4 :(得分:2)
另一个例子:考虑.NET中的异步编程。如果您使用像C#这样的语言并且需要通过Begin / End API进行一系列异步(非阻塞)I / O调用,那么为了调用两个操作Foo
和Bar
,顺序,您必须公开两个方法(BeginFooAndBar
,EndFooAndBar
),其中BeginFooAndBar
调用BeginFoo
并将回调传递给Intermediate
,{{1然后调用Intermediate
,你必须通过线程化中间值和BeginBar
状态信息,如果你想要围绕整个事件IAsyncResult
- try
块,祝你好运,你需要在三个地方复制异常处理代码,哎呀,这真是一团糟。
然后使用F#,有catch
类型,构建在可组合的功能延续之上,因此您可以编写例如。
async
或者你有什么,而且很简单,如果你想在它周围添加一个let AsyncFooAndBar() = async {
let! x = Async.FromBeginEnd(BeginFoo, EndFoo)
let! y = Async.FromBeginEnd(BeginBar, EndBar, x)
return y * 2 }
- try
,那么代码就是一种方法,而不是分散在三种方法中只需在其周围放置catch
- try
即可。
答案 5 :(得分:1)
这是一个真实的例子。住在你家里的所有人的名字都是你家里所有男性的名字,以及你家里所有女性的名单。
这是可组合的,因为这两个子问题中的每一个都可以独立解决而不会干扰解决另一个子问题。
另一方面,许多配方不可组合,因为步骤必须按特定顺序完成,并依赖于其他步骤的结果。你必须在打蛋前打破鸡蛋!
答案 6 :(得分:1)
可组合性允许开发人员社区不断提高抽象级别,而不是链接到基础层。