“无积分”的优点和缺点是什么?函数式编程的风格?
我知道在某些语言(Haskell?)中,努力是实现无点风格,或者从不按名称显式引用函数参数。这对我来说是一个非常难以掌握的概念,但它可能会帮助我理解这种风格的优点(甚至可能是缺点)。谁能解释一下吗?
I know that in some languages (Haskell?) the striving is to achieve point-free style, or to never explicitly refer to function arguments by name. This is a very difficult concept for me to master, but it might help me to understand what the advantages (or maybe even disadvantages) of that style are. Can anyone explain?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
无点风格被一些作者认为是终极函数式编程风格。简单来说,
t1 -> 类型的函数t2
描述从t1
类型的一个元素到t2
类型的另一个元素的转换。这个想法是,“有意义的”函数(使用变量编写)强调元素(当您编写\x -> ... x ...
时,您正在描述元素x
发生了什么),而“无点”函数(不使用变量表达)则强调变换本身,作为更简单变换的组合。无点风格的拥护者认为,转换确实应该是中心概念,而有点符号虽然易于使用,但却分散了我们对这一崇高理想的注意力。无点函数式编程已经存在很长时间了。自 1924 年 Moses Schönfinkel 的开创性工作以来,研究组合逻辑的逻辑学家已经知道了这一点,并且是 Robert Feys 和 Haskell Curry 在 20 世纪 50 年代首次研究 ML 类型推理的基础。
从一组富有表现力的基本组合器构建函数的想法非常有吸引力,并且已应用于各个领域,例如源自 APL,或解析器组合器库,例如 Haskell 的 秒差距。无点编程的著名倡导者是 John Backus。在 1978 年的演讲《编程能否从冯·诺依曼风格中解放出来?》中,他写道:
所以他们在这里。无点编程的主要优点是它们强制采用结构化组合器风格,使等式推理变得自然。 “Squiggol”运动的支持者特别宣传了等式推理(参见 [1] [2]),并且确实使用了相当一部分的无点组合器和计算/重写/推理规则。
最后,无点编程在哈斯克尔派中流行的原因之一是它与 类别理论。在范畴论中,态射(可以看作“对象之间的变换”)是研究和计算的基本对象。虽然部分结果允许以有点风格执行特定类别的推理,但构建、检查和操作箭头的常见方法仍然是无点风格,并且其他语法(例如字符串图)也表现出这种“无点性”。提倡“编程代数”方法的人和编程中类别的用户之间存在相当紧密的联系(例如香蕉论文 [2] 的作者是铁杆类别主义者)。
您可能对 Haskell wiki 的 Pointfree 页面感兴趣。
pointfree 风格的缺点相当明显:阅读起来确实很痛苦。尽管存在许多令人恐惧的阴影、alpha 等价等问题,但我们仍然喜欢使用变量的原因是,它是一种非常容易阅读和思考的表示法。总体思想是,复杂函数(使用引用透明语言)就像一个复杂的管道系统:输入是参数,它们进入一些管道,应用于内部函数,重复(
\x -> (x,x)
) 或忘记了(\x -> ()
,管道无处通向)等。并且变量表示法很好地隐含了所有这些机制:你给出一个输入的名称,输出的名称(或辅助计算),但您不必描述所有的管道计划,其中小管道不会成为较大管道的障碍,等等。在像\(f,x,y) ->; 这样短的东西中进行管道连接((x,y), fxy)
太棒了。您可以单独跟踪每个变量,或者读取每个中间管道节点,但您永远不必一起查看整个机器。当您使用无点样式时,所有管道都是明确的,您必须写下所有内容,然后再查看,有时它简直是丑陋的。PS:这种管道愿景与堆栈编程语言密切相关,堆栈编程语言可能是(几乎)使用中最没有意义的编程语言。我建议尝试在其中进行一些编程,只是为了感受它(就像我建议逻辑编程一样)。请参阅因子、猫或可敬的Forth。
The point-free style is considered by some author as the ultimate functional programming style. To put things simply, a function of type
t1 -> t2
describes a transformation from one element of typet1
into another element of typet2
. The idea is that "pointful" functions (written using variables) emphasize elements (when you write\x -> ... x ...
, you're describing what's happening to the elementx
), while "point-free" functions (expressed without using variables) emphasize the transformation itself, as a composition of simpler transforms. Advocates of the point-free style argue that transformations should indeed be the central concept, and that the pointful notation, while easy to use, distracts us from this noble ideal.Point-free functional programming has been available for a very long time. It was already known by logicians which have studied combinatory logic since the seminal work by Moses Schönfinkel in 1924, and has been the basis for the first study on what would become ML type inference by Robert Feys and Haskell Curry in the 1950s.
The idea to build functions from an expressive set of basic combinators is very appealing and has been applied in various domains, such as the array-manipulation languages derived from APL, or the parser combinator libraries such as Haskell's Parsec. A notable advocate of point-free programming is John Backus. In his 1978 speech "Can Programming Be Liberated From the Von Neumann Style ?", he wrote:
So here they are. The main advantage of point-free programming are that they force a structured combinator style which makes equational reasoning natural. Equational reasoning has been particularly advertised by the proponents of the "Squiggol" movement (see [1] [2]), and indeed use a fair share of point-free combinators and computation/rewriting/reasoning rules.
Finally, one cause for the popularity of point-free programming among Haskellites is its relation to category theory. In category theory, morphisms (which could be seen as "transformations between objects") are the basic object of study and computation. While partial results allow reasoning in specific categories to be performed in a pointful style, the common way to build, examine and manipulate arrows is still the point-free style, and other syntaxes such as string diagrams also exhibit this "pointfreeness". There are rather tight links between the people advocating "algebra of programming" methods and users of categories in programming (for example the authors of the banana paper [2] are/were hardcore categorists).
You may be interested in the Pointfree page of the Haskell wiki.
The downside of pointfree style is rather obvious: it can be a real pain to read. The reason why we still love to use variables, despite the numerous horrors of shadowing, alpha-equivalence etc., is that it's a notation that's just so natural to read and think about. The general idea is that a complex function (in a referentially transparent language) is like a complex plumbing system: the inputs are the parameters, they get into some pipes, are applied to inner functions, duplicated (
\x -> (x,x)
) or forgotten (\x -> ()
, pipe leading nowhere), etc. And the variable notation is nicely implicit about all that machinery: you give a name to the input, and names on the outputs (or auxiliary computations), but you don't have to describe all the plumbing plan, where the small pipes will go not to be a hindrance for the bigger ones, etc. The amount of plumbing inside something as short as\(f,x,y) -> ((x,y), f x y)
is amazing. You may follow each variable individually, or read each intermediate plumbing node, but you never have to see the whole machinery together. When you use a point-free style, all the plumbing is explicit, you have to write everything down, and look at it afterwards, and sometimes it's just plain ugly.PS: this plumbing vision is closely related to the stack programming languages, which are probably the least pointful programming languages (barely) in use. I would recommend trying to do some programming in them just to get of feeling of it (as I would recommend logic programming). See Factor, Cat or the venerable Forth.
我相信目的是简洁并将流水线计算表达为函数的组合,而不是考虑线程参数。简单的例子(在 F# 中) - 给出:
使用如下:
我们可以将“平方和”函数表示为:
并使用如下:
或者我们可以通过管道 x 来定义它: 以
这种方式编写,很明显 x 正在被传入仅通过一系列函数“线程化”。直接合成看起来更好:
这更简洁,并且是思考我们正在做的事情的不同方式;组合函数而不是想象参数流动的过程。我们没有描述
sumsqr
的工作原理。我们正在描述它是什么。PS:了解组合的一个有趣方法是尝试使用连接语言进行编程,例如 Forth、Joy、Factor 等。这些可以被认为只是组合(Forth
: sumsqr sqr sum ;< /code>),其中单词之间的空格是组合运算符。
PPS:也许其他人可以评论性能差异。在我看来,组合可以通过使编译器更清楚地知道不需要像管道一样生成中间值来减少 GC 压力;帮助使所谓的“森林砍伐”问题更容易处理。
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:
Used like:
We could express a "sum of squares" function as:
And use like:
Or we could define it by piping x through:
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:
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.
虽然我被无点概念所吸引并将其用于某些事情,并且同意之前所说的所有积极因素,但我发现这些事情是消极的(一些在上面详细说明):
较短的符号减少了冗余;在高度结构化的组合中(ramda.js 风格,或者 Haskell 中的无点,或者任何连接语言),代码读取比线性扫描一堆
const
绑定并使用符号荧光笔更复杂查看哪个绑定进入其他哪些下游计算。除了树结构与线性结构之外,描述性符号名称的丢失使得该函数难以直观地掌握。当然,树结构和命名绑定的丢失也有很多优点,例如,函数会感觉更通用 - 不通过所选符号名称绑定到某些应用程序域 - 并且树结构在语义上甚至存在如果绑定已布局,并且可以按顺序理解(lisp let/let* 样式)。当只是通过管道传输或组合一系列函数时,无点是最简单的,因为这也会产生我们人类易于遵循的线性结构。然而,通过多个接收者进行一些临时计算是很乏味的。有各种包装成元组、透镜和其他艰苦的机制,只是为了使某些计算可访问,否则这只是某些值绑定的多次使用。当然,重复的部分可以作为一个单独的函数提取出来,也许这是一个好主意,但是某些非短函数也有参数,即使提取出来,它的参数也必须以某种方式通过两个应用程序进行线程化,然后可能需要记住该函数,以免实际重复计算。人们会使用很多
converge
、lens
、memoize
、useWidth
等。JavaScript 特定:更难随意调试。通过
let
绑定的线性流程,可以轻松地在任意位置添加断点。使用无点样式,即使以某种方式添加断点,值流也很难阅读,例如。您不能只查询或将鼠标悬停在开发控制台中的某些变量上。另外,由于 point-free 不是 JS 原生的,ramda.js 或类似的库函数会使堆栈变得相当模糊,特别是在强制柯里化的情况下。代码脆弱,尤其是在规模较大的系统和生产中。如果出现新的需求,那么上述缺点就会发挥作用(例如,下一个维护者在几周后可能就是你自己,很难阅读代码,也很难跟踪数据流进行检查)。但最重要的是,即使是看似小而无辜的新需求也可能需要完全不同的代码结构。也许有人会说,这是一件好事,因为它将是新事物的清晰表示,但重写大量无点代码非常耗时,而且我们还没有提到测试。因此,人们认为,更松散、结构性较差、基于词汇分配的编码可以更快地重新调整用途。特别是如果编码是探索性的,并且在具有奇怪约定(时间等)的人类数据领域中,很少能够 100% 准确地捕获,并且可能总是存在即将到来的要求更准确地处理某些内容或更符合用户需求的请求。对于客户来说,无论哪种方法能够带来更快的转变都非常重要。
While I'm attracted to the point-free concept and used it for some things, and agree with all the positives said before, I found these things with it as negative (some are detailed above):
The shorter notation reduces redundancy; in a heavily structured composition (ramda.js style, or point-free in Haskell, or whatever concatenative language) the code reading is more complex than linearly scanning through a bunch of
const
bindings and using a symbol highlighter to see which binding goes into what other downstream calculation. Besides the tree vs linear structure, the loss of descriptive symbol names makes the function hard to intuitively grasp. Of course both the tree structure and the loss of named bindings also have a lot of positives as well, for example, functions will feel more general - not bound to some application domain via the chosen symbol names - and the tree structure is semantically present even if bindings are laid out, and can be comprehended sequentially (lisp let/let* style).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.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.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.
对于无点变体,即串联编程语言,我必须写:
我对乔伊有一点经验。喜悦是一个非常简单而美丽的列表概念。将问题转换为 Joy 函数时,您必须将大脑分成两部分,一部分负责堆栈管道工作,另一部分负责 Joy 语法中的解决方案。堆栈总是从后面处理。由于合成包含在 Joy 中,因此合成组合器没有计算时间。
To the pointfree variant, the concatenative programming language, i have to write:
I had a little experience with Joy. Joy is a very simple and beautiful concept with lists. When converting a problem into a Joy function, you have to split your brain into a part for the stack plumbing work and a part for the solution in the Joy syntax. The stack is always handled from the back. Since the composition is contained in Joy, there is no computing time for a composition combiner.