函数式编程中"无点"风格的优缺点是什么?

Eli*_*der 66 f# functional-programming pointfree

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

gas*_*che 74

一些作者认为无点样式是最终的函数式编程风格.简单地说,类型函数t1 -> t2描述了从一个类型t1元素到另一个类型元素的转换t2.我们的想法是"有点"函数(使用显式变量编写)强调元素(当你编写时\x -> ... x ...,你正在描述元素发生了什么x),而"无点"函数(不使用变量表示)强调转换本身,作为更简单的变换的组合.无点风格的倡导者认为变换确实应该是核心概念,有点使用的符号虽然易于使用,但却让我们分散了这种崇高的理想.

无点函数编程已经存在很长时间了.它已经由研究了逻辑学家称为组合逻辑,因为由摩西·施菲克尔在1924年的开创性工作,并已在什么将成为20世纪50年代由罗伯特·费斯和...哈斯克尔库里ML类型推断的第一项研究的基础.

从一个表达组基本组合子的构建函数的想法是非常有吸引力并且在各种领域,如衍生自所述阵列操纵语言已经施加APL,或解析器组合库如Haskell的秒差距.John Backus是一位着名的无点编程倡导者.他在1978年的演讲"可以从冯·诺伊曼风格中解放出来的节目吗?"中写道:

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

所以他们在这里.无点编程的主要优点是它们强制结构化的组合式,使得等式推理自然.等式推理已经由"Squiggol"运动的拥护者被特别公布(参见[1] [2]),并且实际上使用无点组合程序和计算/重写/推理规则的公平份额.

最后,在haskellites中无点编程普及的一个原因是它与类别理论的关系.在范畴论中,态射(可以被视为"物体之间的转换")是研究和计算的基本对象.虽然部分结果允许在贴题风格来执行推理在特定类别,来构建,检查和操纵箭头常见的方式仍是自由点式,和其它语法如图串也表现出这种"pointfreeness".倡导"编程代数"方法的人与编程中的类别用户之间存在相当紧密的联系(例如香蕉论文的作者[2]是核心分类).

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

无点风格的缺点是相当明显的:阅读可能是一个真正的痛苦.我们仍然喜欢使用变量的原因,尽管存在大量的阴影,alpha等价等等,这是一个让你自然而然地阅读和思考的符号.总的想法是一个复杂的功能(在透明的参考语言)就像是一个复杂的管道系统:输入为参数,他们遇到了一些管道,应用到内部功能,复制(\x -> (x,x))或遗忘(\x -> (),管龙头无处并且变量符号很好地隐含了所有机器:你给输入命名,输出上的名字(或辅助计算),但你不必描述所有的管道计划,其中小管子不会成为较大的管道​​等的障碍.管道内的管道数量很少\(f,x,y) -> ((x,y), f x y)令人惊讶.您可以单独跟踪每个变量,也可以读取每个中间管道节点,但您不必一起看到整个机器.当你使用无点样式时,它都是明确的,你必须把所有东西都写下来,然后看看它,有时它只是简单的丑陋.

PS:这个管道视觉与堆栈编程语言密切相关,这些语言可能是使用中最少有点编程的语言(几乎没有).我建议尝试在它们中进行一些编程以获得它的感觉(因为我建议使用逻辑编程).请参阅因子,或古老的Forth.


Ash*_*eyF 56

我认为目的是简洁,并将流水线计算表达为函数的组合,而不是考虑通过线程论证.简单的例子(在F#中) - 给出:

let sum = List.sum
let sqr = List.map (fun x -> x * x)
Run Code Online (Sandbox Code Playgroud)

使用如下:

> sum [3;4;5]
12
> sqr [3;4;5]
[9;16;25]
Run Code Online (Sandbox Code Playgroud)

我们可以将"平方和"表示为:

let sumsqr x = sum (sqr x)
Run Code Online (Sandbox Code Playgroud)

并使用如下:

> sumsqr [3;4;5]
50
Run Code Online (Sandbox Code Playgroud)

或者我们可以通过管道x来定义它:

let sumsqr x = x |> sqr |> sum
Run Code Online (Sandbox Code Playgroud)

以这种方式编写,显然x被传入只是通过一系列函数"线程化".直接构图看起来更好:

let sumsqr = sqr >> sum
Run Code Online (Sandbox Code Playgroud)

这更简洁,它是一种不同的思考我们正在做的事情的方式; 编写函数而不是想象论证流程的过程.我们没有描述如何sumsqr运作.我们正在描述它什么.

PS:一种有趣的方法来尝试使用Forth,Joy,Factor等连接语言进行编程.这些可以被认为只是组合(Forth : sumsqr sqr sum ;),其中单词之间的空间是作曲家.

PPS:也许其他人可以评论性能差异.在我看来,组合可能会降低GC压力,使编译器更明显的是不需要像流水线那样产生中间值; 帮助使所谓的"砍伐森林"问题更容易处理.

  • 关于改进编译的部分根本不是真的.在大多数语言中,无点样式实际上会降低性能.Haskell在很大程度上依赖于优化,因为它是使这些东西的成本可以承受的唯一方法.充其量,那些组合器被内联,你得到一个相同的有点版本. (12认同)
  • 我所说的减少GC压力的"砍伐森林"是指编译器可以避免分配中间值(例如来自`sqr`的列表),当它清楚地表明它只是被传递给`sum`来构造结果时; 将函数组合作为_hint_来完成它.`List.sum`实际上是`List.fold(+)0`或`List.fold(fun sx - > s + x)`.使用地图进行组合:`List.map(fun x - > x*x)>> List.fold(fun sx - > s + x)`或者可以融合成一个:`List.fold(fun sx - > s + x*x)0`,避免分配.请参阅:https://link.springer.com/content/pdf/10.1007/3-540-19027-9_23.pdf (2认同)

Rob*_*era 6

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

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

  2. Point-free is simplest when just piping through or composing a series of functions, as this also results in a linear structure that we humans find easy to follow. However, threading some interim calculation through multiple recipients is tedious. There are all kinds of wrapping into tuples, lensing and other painstaking mechanisms go into just making some calculation accessible, that would otherwise be just the multiple use of some value binding. Of course the repeated part can be extracted out as a separate function and maybe it's a good idea anyway, but there are also arguments for some non-short functions and even if it's extracted, its arguments will have to be somehow threaded through both applications, and then there may be a need for memoizing the function to not actually repeat the calculation. One will use a lot of converge, lens, memoize, useWidth etc.

  3. JavaScript specific: harder to casually debug. With a linear flow of let bindings, it's easy to add a breakpoint wherever. With the point-free style, even if a breakpoint is somehow added, the value flow is hard to read, eg. you can't just query or hover over some variable in the dev console. Also, as point-free is not native in JS, library functions of ramda.js or similar will obscure the stack quite a bit, especially with the obligate currying.

  4. Code brittleness, especially on nontrivial size systems and in production. If a new piece of requirement comes in, then the above disadvantages get into play (eg. harder to read the code for the next maintainer who may be yourself a few weeks down the line, and also harder to trace the dataflow for inspection). But most importantly, even something seemingly small and innocent new requirement can necessitate a whole different structuring of the code. It may be argued that it's a good thing in that it'll be a crystal clear representation of the new thing, but rewriting large swaths of point-free code is very time consuming and then we haven't mentioned testing. So it feels that the looser, less structured, lexical assignment based coding can be more quickly repurposed. Especially if the coding is exploratory, and in the domain of human data with weird conventions (time etc.) that can rarely be captured 100% accurately and there may always be an upcoming request for handling something more accurately or more to the needs of the customer, whichever method leads to faster pivoting matters a lot.

  • 关于第 3 点,`const tap = x => (console.log(x), x);` 会让你省去很多很多痛苦(虽然不是完全没有痛苦)。 (2认同)
  • 每个人都求助于使用 Tap,尤其是。具有可观察量,但它是您需要添加然后删除的东西,而在一系列“const”绑定中,您只需单击开发工具中的行 - 但 ofc 的最大代价是它不是无点的 (2认同)