函数编程中“无点”样式的优缺点是什么?


69

我知道在某些语言中(Haskell?),努力是为了实现无点样式,或者永远不要通过名称显式地引用函数参数。对于我来说,这是一个很难理解的概念,但它可能有助于我理解该样式的优点(甚至是缺点)。谁能解释?

Answers:


57

我相信这样做的目的是简洁明了,并将流水线计算表达为功能的组合,而不是考虑通过线程进行参数传递。简单示例(在F#中)-给出:

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

像这样使用:

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

我们可以将“平方和”函数表示为:

let sumsqr x = sum (sqr x)

并使用像:

> sumsqr [3;4;5]
50

或者我们可以通过以下方式定义x:

let sumsqr x = x |> sqr |> sum

这样写,很明显x只是通过一系列函数传递给“线程”。直接合成看起来更好:

let sumsqr = sqr >> sum

这更加简洁,是我们正在做的事情的另一种思考方式。组成函数,而不是想象经过的论证过程。我们没有描述sumsqr工作原理。我们正在描述它什么。

PS:了解合成的一种有趣方法是尝试使用诸如Forth,Joy,Factor等连接语言进行编程。这些可以被认为只是合成(Forth : sumsqr sqr sum ;),其中单词之间的空格是合成算符

PPS:也许其他人可以对性能差异发表评论。这在我看来,组合物可通过使其更加降低GC的压力明显的编译器,没有必要生产中间值作为流水线; 帮助解决所谓的“森林砍伐”问题。


12
关于改进编译的部分根本不正确。在大多数语言中,无点样式实际上会降低性能。Haskell完全依赖优化,因为这是使这些事情的成本可以承受的唯一途径。充其量,这些组合器会内联,您会得到一个等效的有意义的版本。
加什2011年

2
我所说的“砍伐森林”减轻了GC的压力,是因为sqr当很明显只是sum为了构造结果而传递中间值时,编译器可以避免分配中间值(例如的列表)。以功能组合为提示List.sum是真的List.fold (+) 0还是List.fold (fun s x -> s + x)。与地图组成为:List.map (fun x -> x * x) >> List.fold (fun s x -> s + x)或可以融合为一个:List.fold (fun s x -> s + x * x) 0,避免分配。参见:link.springer.com/content/pdf/10.1007/3-540-19027-9_23.pdf
AshleyF,

75

无点样式被某些作者视为最终的函数式编程样式。简单来说,类型函数t1 -> t2描述了从一个类型t1元素到另一种类型元素的转换t2。这个想法是,“有指向性”函数(使用变量编写)强调元素(当您编写时\x -> ... x ...,您正在描述元素所发生的事情x),而“无点”函数(不使用变量表示)强调变换本身,因为简单转换的组成。无分风格的拥护者认为,转换确实应该是中心概念,并且有针对性的符号虽然易于使用,

无点功能编程已经有很长时间了。研究组合逻辑的逻辑学家已经知道了自1924年MosesSchönfinkel的开创性工作以来,并且已经成为罗伯特·菲斯和Haskell Curry在1950年代首次进行ML类型推断的研究的基础。

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

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

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

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

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

无点样式的缺点非常明显:阅读起来可能真是痛苦。尽管存在许多阴影,alpha等效等恐怖现象,我们仍然喜欢使用变量的原因是,它是一种易于阅读和思考的符号。一般的想法是,复杂的功能(使用参照透明语言)就像复杂的管道系统:输入是参数,它们进入一些管道,应用于内部函数,重复(\x -> (x,x))或被遗忘(\x -> (),无处引出的管道)等等。所有机器的变量表示法都很好地隐含:您为输入指定一个名称,并在输出上指定一个名称(或辅助计算),但不必描述所有的管道计划中,小管道将不会成为大管道的障碍,等等。管道内的水管数量如此之短,\(f,x,y) -> ((x,y), f x y)真是令人惊讶。您可以单独跟踪每个变量,也可以读取每个中间管道节点,但是您不必一起查看整个机器。当使用无点样式时,所有管道都是显式的,您必须将所有内容写下来,然后再进行查看,有时这很丑陋。

PS:这种深入的愿景与堆栈编程语言密切相关,而堆栈编程语言可能是使用程度最低(很少)的编程语言。我建议尝试在其中进行一些编程,以便获得它的感觉(就像我建议逻辑编程一样)。参见FactorCat或尊贵的Forth


“使用无点样式时,所有内容都是明确的。”→这里不是很有意义吗?或者:隐式
Magne

我认为原样的句子是正确的。在无点样式中,必须非常清楚函数中从输入到输出的值流,而有点样式则依靠名称来避免这种情况。例如,没有标记,x并且y在右侧有重复标记,它们仅出现两次。如果您尝试以无点样式实现此功能,那么您将看到必须对此进行更明确的说明。
加什

我对整个段落还是有些困惑,因为您之前写过The idea is that "pointful" functions (written using explicit variables)..
Magne

1
是的:当您有变量时,变量是显式的,但是数据流管道是隐式的。在无点样式中,没有变量,但是必须明确显示管道。(编辑:为避免混淆,我删除了您引用的配方中的“显式”,谢谢。)
gasche

感谢您的澄清。难道是正确的理解,第二个到最后的段落开始被提免费的点式的,但当时主要谈论贴题风格,与描述结束前免费点对点式的?如果是这样,那么我也许可以使上下文切换更清晰(例如The general idea ...,什么?),或者将段落拆分。为了避免混乱。
Magne

5

尽管我被无点概念所吸引,并在某些事情上使用了它,并同意前面提到的所有积极观点,但我发现这些观点都是消极的(上面有一些详细介绍):

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

  2. 仅通过管道传递或组合一系列功能时,无点是最简单的,因为这也导致我们人类容易遵循的线性结构。但是,通过多个接收者进行一些临时计算很繁琐。元组有各种各样的包装,镜头和其他艰苦的机制只是使一些计算变得可访问,否则将仅仅是多次使用某些值绑定。当然,重复的部分可以作为一个单独的函数提取出来,也许还是个好主意,但是对于某些非短函数也有参数,即使将其提取出来,也必须以某种方式将其参数传递给两个应用程序,然后可能需要记住该功能以不实际重复计算。一个会用很多convergelensmemoizeuseWidth等。

  3. 特定于JavaScript:更难随便调试。借助线性let绑定流,可以轻松地在任何地方添加断点。对于无点样式,即使以某种方式添加了断点,也很难读取值流,例如。您不仅可以在开发控制台中查询或将鼠标悬停在某个变量上。而且,由于无点不是JS的本机,因此ramda.js或类似的库函数会相当程度地使堆栈模糊,尤其是在使用强制性时。

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


2
关于第3点,const tap = x => (console.log(x), x);将为您省去很多痛苦(尽管并非完全没有痛苦)。
贾里德·史密斯

1
每个人都诉诸于使用水龙头esp。带有可观察对象,但这是您需要添加然后删除的内容,而在一系列const绑定中,您只需在开发工具中单击该行即可,但是价格高昂并不是因为它并非毫无意义
Robert Monfera

然后将调用放在自己的行上,并使用预处理程序指令或其他构建步骤将其删除以用于非开发构建。这很笨拙,以至于我不称其为“已解决的问题”,但并不是很难,我愿意打赌我的JS代码库中充斥着对的注释掉的调用tap
贾里德·史密斯

这是一个真正伟大而又翔实的答案,其观点很少被谈论。
Magne

-1

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


您不应该在此处写评论作为答案。这不是讨论论坛。请阅读指南。
本特·特兰伯格

我喜欢无重点的风格。这对乔伊的风格有用吗?
fpstefan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.