我知道在某些语言(比如 Haskell)中,追求的是点自由风格,或者从来不显式地引用函数参数名称。这对我来说是一个非常难以掌握的概念,但它可能有助于我理解该风格的优势(甚至缺点)。是否有人能够解释一下呢?
我知道在某些语言(比如 Haskell)中,追求的是点自由风格,或者从来不显式地引用函数参数名称。这对我来说是一个非常难以掌握的概念,但它可能有助于我理解该风格的优势(甚至缺点)。是否有人能够解释一下呢?
所以这就是它们。点无编程的主要优势在于强制使用结构化组合风格,使等式推理自然。等式推理特别受到“Squiggol”运动的支持者(见[1] [2])的宣传,并确实使用了相当比例的点无组合子和计算/重写/推理规则。
最后,点无编程在 Haskellites 中广受欢迎的原因之一是它与范畴论的关系。在范畴论中,态射(可以看作是"对象之间的变换")是研究和计算的基本对象。虽然部分结果允许在特定类别中进行点有样式的推理,但通常建立、检查和操作箭头的方式仍然是点无样式,而其他语法,如字符串图表也展示了这种"点无风格"。在编程中,提倡“代数编程”方法的人和范畴论用户之间存在紧密的联系(例如,香蕉论文[2]的作者是/曾经是核心范畴论者)。
你可能会对Haskell维基上的Pointfree页面感兴趣。
点无风格的缺点非常明显:它很难读。尽管存在许多阴影、α-等价性等恐怖,我们仍然喜欢使用变量,因为它是一种自然而然的符号,易于阅读和思考。总体思路是,在一个引用透明的语言中,一个复杂的函数就像一个复杂的管道系统:输入是参数,它们进入一些管道,被应用于内部函数,重复(\x -> (x,x)
)或遗忘(\x -> ()
, 不导向任何地方的管道),等等。变量符号在所有这些机制上都有很好的隐含性:你给输入命名,输出(或辅助计算)命名,但你不必描述所有的管道计划,小管道将去哪里以不妨碍更大的管道等等。即使在像\(f,x,y) -> ((x,y), f x y)
这样简短的内容中,所包含的管道数量也是惊人的。你可以单独跟踪每个变量,或者阅读每个中间的管道节点,但你永远不必看到整个管道系统。当你使用点无风格时,所有的管道都是显式的,你必须把所有的东西写下来,然后再看一遍,有时它就是很丑陋。
PS:这个管道视觉与堆栈编程语言密切相关,它们可能是使用最少的编程语言(勉强)。我建议尝试在其中进行一些编程,以便对其有所了解(就像我建议逻辑编程一样)。请参见Factor,Cat或可敬的Forth。
我相信其目的是简洁明了地表达流水线计算,将函数组合视为管道计算,而不是考虑通过线程传递参数。下面是一个简单的例子(使用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”如何工作,而是描述它“是什么”。:sumsqr sqr sum;
)构成,其中单词之间的空格是组合运算符。sqr
中的列表),当明确它仅被传递给sum
以构建结果时;将函数组合视为_提示_来执行此操作。List.sum
实际上是List.fold (+) 0
或List.fold (fun s x -> s + x)
。与map组合是:List.map (fun x -> x * x) >> List.fold (fun s x -> s + x)
,或者可以融合成一个:List.fold (fun s x -> s + x * x) 0
,从而避免了分配。参见:https://link.springer.com/content/pdf/10.1007/3-540-19027-9_23.pdf - AshleyF虽然我很喜欢点无的概念并在某些方面使用它,也同意之前说过的所有正面评价,但我发现以下一些问题是负面的(有些已经详细说明):
缩短符号表示可以减少冗余;在结构复杂的组合(例如ramda.js风格、Haskell中的点无关函数或其他连接语言),代码阅读比线性扫描一堆const
绑定并使用符号高亮器查看哪个绑定进入哪个下游计算更加复杂。除了树形结构和线性结构之外,失去描述性符号名称也使得函数难以直观地理解。当然,树形结构和命名绑定的丧失也有很多积极的方面,例如,函数将感觉更加通用——不会通过选择的符号名称绑定到某个应用程序域——即使布局了绑定,树形结构也是语义上存在的,并且可以按顺序理解(lisp let/let*风格)。
仅在通过管道传递或组合一系列函数时,点无关函数最简单,因为这也会导致我们人类容易跟随的线性结构。但是,在多个接收者中穿过一些中间计算是乏味的。有各种包装成元组、镜头等费力的机制,只是为了使一些计算可访问,否则就是一些值绑定的多次使用。当然,重复部分可以作为单独的函数提取出来,也许这是个好主意,但也有一些非短函数的论点,即使它被提取了,其参数也必须通过两个应用程序以某种方式穿过,然后可能需要记忆函数来不实际重复计算。一个将使用很多converge
、lens
、memoize
、useWidth
等。
JavaScript特定:更难调试。用线性流的let
绑定,可以在任何地方添加断点。使用点无关风格,即使某个断点被添加,值流也很难读取,例如,您不能仅查询或悬停在开发控制台中的某个变量上。此外,由于点无关不是JS本身的功能,ramda.js或类似库函数会相当模糊堆栈,尤其是在强制柯里化时。
代码脆弱性,尤其是在非平凡大小系统和生产中。如果有新的要求,那么以上缺点就会发挥作用(例如,对于下一个维护者(可能是您自己几周后)来说,更难阅读代码,而且更难跟踪数据流以进行检查)。但最重要的是,即使是看似小而无辜的新要求也可能需要完全不同的代码结构。可以认为这是一件好事,因为它将是新事物的清晰表示,但重写大量的点无关代码非常耗时,然后我们还没有提到测试。因此,感觉松散、不太结构化、基于词汇分配的编码可以更快地重新定位。特别是如果编码是探索性的,并且在人类数据领域中具有奇怪的约定(时间等),很少能够100%准确地捕获,并且可能总会有一个请求来处理更准确或更符合客户需求的内容,那么哪种方法导致更快的转折就很
const tap = x => (console.log(x), x);
将会让你少受很多痛苦(虽然不是完全无痛苦)。 - Jared Smithconst
绑定中,你只需在开发工具中单击该行 - 当然代价很大,就是它不是 point-free。 - Robert Monferatap
的注释调用。 - Jared Smith
x
和y
是重复的,它们只出现了两次。如果您尝试以点无风格的方式实现此函数,则会看到您需要更加明确。 - gasche