70

我知道在某些语言(Haskell?)中,努力是实现无点风格,或者从不通过名称显式引用函数参数。这对我来说是一个很难掌握的概念,但它可能有助于我理解这种风格的优点(甚至可能是缺点)是什么。谁能解释一下?

4

4 回答 4

78

一些作者认为无点风格是最终的函数式编程风格。简单地说,类型函数t1 -> t2描述了从一个类型元素t1到另一个类型元素的转换t2。这个想法是“pointful”函数(使用变量编写)强调元素(当您编写时\x -> ... x ...,您正在描述元素发生了什么x),而“point-free”函数(不使用变量表示)强调转换本身,如简单变换的组合。无点风格的拥护者认为,转换确实应该是中心概念,并且点符号虽然易于使用,

无点函数式编程已经存在很长时间了。自 1924 年 Moses Schönfinkel 的开创性工作以来,研究组合逻辑的逻辑学家已经知道这一点,并且已成为 Robert Feys 和Haskell Curry在 1950 年代首次研究将成为 ML 类型推理的基础。

从一组富有表现力的基本组合器构建函数的想法非常吸引人,并已应用于各个领域,例如从APL派生的数组操作语言,或解析器组合器库,如 Haskell 的Parsec。无点编程的著名倡导者是John Backus。在他 1978 年的演讲“编程可以从冯诺依曼风格中解放出来吗?”中,他写道:

lambda 表达式(及其替换规则)能够定义所有可能类型和任意数量参数的所有可能可计算函数。这种自由和权力既有缺点,也有明显的优点。它类似于传统语言中不受限制的控制语句的力量:不受限制的自由带来混乱。如果一个人不断地发明新的组合形式来适应这种情况,就像在 lambda 演算中可以做到的那样,人们将不会熟悉适合所有目的的少数组合形式的风格或有用属性。正如结构化编程避开许多控制语句以获得具有更简单结构、更好属性和统一方法来理解它们的行为的程序一样,函数式编程也避开了 lambda 表达式、替换、和多种功能类型。因此,它实现了使用具有已知有用属性的熟悉功能形式构建的程序。这些程序的结构如此之好,以至于它们的行为通常可以通过机械使用类似于解决高中代数问题的代数技术来理解和证明。

所以他们在这里。无点编程的主要优点是它们强制使用结构化组合器样式,这使得等式推理自然。“Squiggol”运动的支持者特别宣传了等式推理(参见 [1] [2]),并且确实使用了相当多的无点组合器和计算/重写/推理规则。

最后,无点编程在 Haskellites 中流行的一个原因是它与范畴论的关系。在范畴论中,态射(可以看作是“对象之间的变换”)是研究和计算的基本对象。虽然部分结果允许特定类别的推理以有针对性的方式进行,但构建、检查和操作箭头的常用方式仍然是无点式,其他语法(如字符串图)也表现出这种“无点”。提倡“编程代数”方法的人与编程类别的用户之间存在着相当紧密的联系(例如,香蕉论文 [2] 的作者是/曾经是铁杆分类学家)。

您可能对Haskell wiki的Pointfree 页面感兴趣。

pointfree 风格的缺点是相当明显的:阅读起来真的很痛苦。我们仍然喜欢使用变量的原因,尽管有许多可怕的阴影、alpha 等价等,因为它是一种非常自然地阅读和思考的符号。一般的想法是,一个复杂的函数(在引用透明的语言中)就像一个复杂的管道系统:输入是参数,它们进入一些管道,应用于内部函数,重复(\x -> (x,x))或忘记(\x -> (), 管道不通)等。变量符号很好地隐含了所有这些机器:您为输入命名,并在输出(或辅助计算)上命名,但您不必描述所有管道计划,小管道不会成为大管道的障碍,等等。这么短的东西里面的管道数量\(f,x,y) -> ((x,y), f x y)是惊人的。您可以单独跟踪每个变量,或读取每个中间管道节点,但您永远不必一起查看整个机器。当您使用无点样式时,所有的管道都是明确的,您必须将所有内容写下来,然后再查看,有时它只是简单的丑陋。

PS:这个管道愿景与堆栈编程语言密切相关,堆栈编程语言可能是使用中最不重要的编程语言(几乎没有)。我建议尝试在其中进行一些编程以感受一下(因为我会推荐逻辑编程)。参见FactorCat或可敬的Forth

于 2011-04-15T22:19:55.710 回答
58

I believe the purpose is to be succinct and to express pipelined computations as a composition of functions rather than thinking of threading arguments through. Simple example (in F#) - given:

let sum = List.sum
let sqr = List.map (fun x -> x * x)

Used like:

> sum [3;4;5]
12
> sqr [3;4;5]
[9;16;25]

We could express a "sum of squares" function as:

let sumsqr x = sum (sqr x)

And use like:

> sumsqr [3;4;5]
50

Or we could define it by piping x through:

let sumsqr x = x |> sqr |> sum

Written this way, it's obvious that x is being passed in only to be "threaded" through a sequence of functions. Direct composition looks much nicer:

let sumsqr = sqr >> sum

This is more concise and it's a different way of thinking of what we're doing; composing functions rather than imagining the process of arguments flowing through. We're not describing how sumsqr works. We're describing what it is.

PS: An interesting way to get your head around composition is to try programming in a concatenative language such as Forth, Joy, Factor, etc. These can be thought of as being nothing but composition (Forth : sumsqr sqr sum ;) in which the space between words is the composition operator.

PPS: Perhaps others could comment on the performance differences. It seems to me that composition may reduce GC pressure by making it more obvious to the compiler that there is no need to produce intermediate values as in pipelining; helping make the so-called "deforestation" problem more tractable.

于 2011-04-15T01:35:49.103 回答
7

虽然我被无点概念所吸引并将其用于某些事情,并且同意之前所说的所有积极因素,但我发现这些事情是消极的(有些在上面有详细说明):

  1. 较短的符号减少了冗余;在高度结构化的组合(ramda.js 风格,或 Haskell 中的无点,或任何连接语言)中,代码阅读比线性扫描一堆const绑定并使用符号荧光笔查看哪个绑定进入其他绑定更复杂下游计算。除了树形结构与线性结构外,描述性符号名称的丢失使得函数难以直观地掌握。当然,树结构和命名绑定的丢失也有很多积极的一面,例如,函数会感觉更通用 - 不会通过选择的符号名称绑定到某些应用程序域 - 而且树结构甚至在语义上也存在如果绑定已布局,并且可以按顺序理解(lisp let/let* 样式)。

  2. 当只是通过管道或组合一系列函数时,无点是最简单的,因为这也会导致我们人类发现易于遵循的线性结构。但是,通过多个收件人进行一些临时计算是乏味的。元组中有各种各样的包装,透镜和其他艰苦的机制只是为了使一些计算可以访问,否则只是对一些值绑定的多次使用。当然,重复的部分可以作为一个单独的函数提取出来,无论如何这可能是个好主意,但也有一些非短函数的参数,即使它被提取,它的参数也必须以某种方式通过两个应用程序线程化,然后可能需要记住函数以不实际重复计算。一个人会用很多converge, lens,等memoize_useWidth

  3. 特定于 JavaScript:更难随意调试。使用线性let绑定流,很容易在任何地方添加断点。使用无点样式,即使以某种方式添加断点,值流也难以阅读,例如。您不能只在开发控制台中查询或将鼠标悬停在某个变量上。此外,由于 point-free 在 JS 中不是原生的,ramda.js 或类似的库函数会相当模糊堆栈,尤其是在强制柯里化的情况下。

  4. 代码脆弱,尤其是在非平凡的大小系统和生产中。如果出现新的需求,那么上述缺点就会发挥作用(例如,下一个维护者可能会在几周后成为您自己的维护者更难阅读代码,并且也更难跟踪数据流以进行检查)。但最重要的是,即使是一些看似小而无害的新需求,也可能需要完全不同的代码结构。有人可能会说这是一件好事,因为它将清晰地表示新事物,但是重写大量无点代码非常耗时,而且我们还没有提到测试。所以感觉更松散、结构更少、基于词汇分配的编码可以更快地重新利用。特别是如果编码是探索性的,

于 2017-10-18T09:54:15.413 回答
-1

对于无点变体,连接编程语言,我必须写:
我对 Joy 有一点经验。Joy 是一个非常简单而美丽的概念,带有列表。将问题转换为 Joy 函数时,您必须将大脑分成一部分用于堆栈管道工作,另一部分用于 Joy 语法中的解决方案。堆栈总是从后面处理。由于合成包含在 Joy 中,合成组合器没有计算时间。

于 2020-10-21T21:46:36.543 回答