功能与命令的混合体
纯函数式编程语言不允许可变数据,但某些计算可以更自然/直观地以命令式方式表达,或者算法的命令式版本可能更有效。我知道大多数函数式语言都不是纯粹的,并且允许您分配/重新分配变量并执行命令性的操作,但通常不鼓励这样做。
我的问题是,为什么不允许在局部变量中操作局部状态,但要求函数只能访问它们自己的局部变量和全局常量(或者只是在外部作用域中定义的常量)?这样,所有函数都保持引用透明性(给定相同的参数,它们总是给出相同的返回值),但在函数内,计算可以用命令式术语表达(例如,while 循环)。
IO 等仍然可以通过正常的功能方式完成 - 通过 monad 或传递“世界”或“宇宙”令牌。
Pure functional programming languages do not allow mutable data, but some computations are more naturally/intuitively expressed in an imperative way -- or an imperative version of an algorithm may be more efficient. I am aware that most functional languages are not pure, and let you assign/reassign variables and do imperative things but generally discourage it.
My question is, why not allow local state to be manipulated in local variables, but require that functions can only access their own locals and global constants (or just constants defined in an outer scope)? That way, all functions maintain referential transparency (they always give the same return value given the same arguments), but within a function, a computation can be expressed in imperative terms (like, say, a while loop).
IO and such could still be accomplished in the normal functional ways - through monads or passing around a "world" or "universe" token.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
好问题。我认为答案是,可变局部变量的实用价值有限,但可变堆分配的数据结构(主要是数组)非常有价值,并且构成了许多重要集合的支柱,包括高效的堆栈、队列、集合和字典。因此,仅将突变限制为局部变量不会给纯函数式语言带来突变的任何重要好处。
与此相关的是,交换纯功能数据结构的通信顺序进程提供了两个世界的许多好处,因为顺序进程可以在内部使用突变,例如可变消息队列比任何纯功能队列快约 10 倍。例如,这在 F# 中是惯用的,其中 MailboxProcessor 中的代码使用可变数据结构,但它们之间传递的消息是不可变的。
在这种情况下,排序是一个很好的案例研究。 Sedgewick 的 C 快速排序简短而简单,并且比任何语言中最快的纯函数排序快数百倍。原因是快速排序会就地改变数组。反复无常的当地人无济于事。大多数图算法都是同样的情况。
Good question. I think the answer is that mutable locals are of limited practical value but mutable heap-allocated data structures (primarily arrays) are enormously valuable and form the backbone of many important collections including efficient stacks, queues, sets and dictionaries. So restricting mutation to locals only would not give an otherwise purely functional language any of the important benefits of mutation.
On a related note, communicating sequential processes exchanging purely functional data structures offer many of the benefits of both worlds because the sequential processes can use mutation internally, e.g. mutable message queues are ~10x faster than any purely functional queues. For example, this is idiomatic in F# where the code in a
MailboxProcessor
uses mutable data structures but the messages communicated between them are immutable.Sorting is a good case study in this context. Sedgewick's quicksort in C is short and simple and hundreds of times faster than the fastest purely functional sort in any language. The reason is that quicksort mutates the array in-place. Mutable locals would not help. Same story for most graph algorithms.
简短的回答是:有一些系统可以满足您的需求。例如,您可以使用 Haskell 中的
ST
monad 来完成此操作(如评论中所引用)。ST
monad 方法来自 Haskell 的Control.Monad.ST
。在ST
monad 中编写的代码可以在方便的地方使用引用 (STRef
)。好的部分是,您甚至可以在纯代码中使用 ST monad 的结果,因为它本质上是独立的(这基本上就是您在问题中想要的)。这个独立属性的证明是通过类型系统完成的。
ST
monad 带有一个状态线程参数,通常用类型变量s
表示。当你进行这样的计算时,你将得到一元结果,其类型如下:要实际将其转换为纯结果,你必须使用
你可以读取此类型,如:给我一个计算,其中
s 类型参数并不重要,我可以给你返回计算结果,没有
ST
包袱。这基本上可以防止可变ST
变量逃逸,因为它们会携带s
,而这些s
会被类型系统捕获。这可以对使用底层可变结构实现的纯结构产生良好的效果(例如 向量包)。人们可以在有限的时间内摆脱不变性,以做一些就地改变底层数组的事情。例如,可以将不可变的
Vector
与 impure 结合起来算法包以保留就地排序算法的大部分性能特征,并且仍然获得纯度。在这种情况下,它看起来像:
thaw
和freeze
函数是线性时间复制,但这不会破坏整体 O(n lg n) 运行时间。您甚至可以使用unsafeFreeze
来避免再次线性遍历,因为可变向量不会再次使用。The short answer is: there are systems to allow what you want. For example, you can do it using the
ST
monad in Haskell (as referenced in the comments).The
ST
monad approach is from Haskell'sControl.Monad.ST
. Code written in theST
monad can use references (STRef
) where convenient. The nice part is that you can even use the results of theST
monad in pure code, as it is essentially self-contained (this is basically what you were wanting in the question).The proof of this self-contained property is done through the type-system. The
ST
monad carries a state-thread parameter, usually denoted with a type-variables
. When you have such a computation you'll have monadic result, with a type like:To actually turn this into a pure result, you have to use
You can read this type like: give me a computation where the
s
type parameter doesn't matter, and I can give you back the result of the computation, without theST
baggage. This basically keeps the mutableST
variables from escaping, as they would carry thes
with them, which would be caught by the type system.This can be used to good effect on pure structures that are implemented with underlying mutable structures (like the vector package). One can cast off the immutability for a limited time to do something that mutates the underlying array in place. For example, one could combine the immutable
Vector
with an impure algorithms package to keep the most of the performance characteristics of the in place sorting algorithms and still get purity.In this case it would look something like:
The
thaw
andfreeze
functions are linear-time copying, but this won't disrupt the overall O(n lg n) running time. You can even useunsafeFreeze
to avoid another linear traversal, as the mutable vector isn't used again.“纯函数式编程语言不允许可变数据”……实际上确实如此,您只需识别出它隐藏的位置并了解它的本质即可。
可变性是指两个事物具有相同的名称和互斥的存在时间,以便它们可以被视为“不同时间的同一事物”。但正如每个禅宗哲学家都知道的那样,不存在“不同时间相同的事情”这样的事情。一切都在瞬间停止存在,并由其后继者以可能改变的形式、(可能)无数无限连续的瞬间继承。
在 lambda 演算中,可变性采用以下示例所示的形式:(λx (λx f(x)) (x+1)) (x+1),也可以表示为“让 x = x + 1 in let x = x + 1 in f(x)”或只是“x = x + 1, x = x + 1, f(x)”,更类似于 C 的表示法。
换句话说,“lambda演算”的“名称冲突”实际上是命令式编程的变相“更新”。在禅宗(永远是对的)眼中,它们是一体的。
因此,我们将变量的每个时刻和状态称为对象的 Zen Scope。一个带有可变对象的普通作用域相当于许多带有常量、不可变对象的 Zen 作用域,如果它们是第一个,则它们要么被初始化,要么从它们的前任继承(如果它们不是)。
当人们说“可变性”时,他们是在错误地识别并混淆这个问题。可变性(正如我们刚刚在这里看到的)完全是一个转移注意力的话题。他们实际上的意思(甚至他们自己也不清楚)是无限的可变性;即出现在循环控制流结构中的类型。换句话说,他们实际上指的是——特别是“命令式”而不是“功能性”——根本不是可变性,而是循环控制流结构以及由此带来的 Zen 范围的无限嵌套。
因此,lambda 演算中缺少的关键特征不被视为可以通过包含像 monad 这样过度劳累和深思熟虑的“解决方案”来补救的东西(尽管这并不排除它完成工作的可能性) )但作为无限项。
控制流结构是未包装(可能无限)决策树结构的包装。分支可能会重新聚合。在相应的展开结构中,它们显示为复制但独立的分支或子树。 Goto 是到子树的直接链接。向后分支到控制流结构的早期部分(循环控制流结构“循环”的起源)的 goto 或分支是到所链接的整个结构的相同形状副本的链接。每个结构对应的是其Universally Unrolled决策树。
更准确地说,我们可以将控制流结构视为位于实际表达式之前的语句,该语句限制该表达式的值。典型的例子是 Landin 的原始案例本身(在他 1960 年的论文中,他试图将命令式语言 lambda 化):让 f(x) 中的 x = 1。 “x = 1”部分是语句,“f(x)”是该语句所条件的值。在类似 C 的形式中,我们可以将其写为 x = 1, f(x)。
更一般地,对应于每个语句S和表达式Q的是表达式S[Q],其表示应用S之后的结果Q。因此,(x = 1)[f(x)] 就是 λx f(x) (x + 1)。 S 环绕 Q。如果 S 包含循环控制流结构,则环绕将是无限的。
当兰丁试图制定这个策略时,当他进入 while 循环时,他碰壁了,并说“哎呀。没关系。”然后又陷入了一个过度劳累和过度思考的解决方案,而这个简单的(回想起来,显而易见的)答案却没有引起他的注意。
while 循环“while (x < n) x = x + 1;” - 具有上述“无限可变性”,本身可以被视为无限包装器,“if (x < n) { x = x + 1; if (x < 1) { x = x + 1; if (x < 1) { x = x + 1 ... } } }”。因此,当它环绕表达式 Q 时,结果是(用类似 C 的表示法)“x < n? (x = x + 1, x < n? (x = x + 1, x < n? (x = x + 1, ...): Q): Q): Q",可以直接以 lambda 形式呈现为“x < n? (λx x < n? λx x < n? (λx·...) (x + 1): Q) (x + 1): Q) (x + 1): Q”。这直接表明了循环性和无限性之间的联系。
这是一个无限表达式,尽管是无限的,但只有有限数量的不同子表达式。正如我们可以认为该表达式有一个通用展开形式 - 类似于上面显示的内容(无限决策树) - 我们也可以认为存在一个最大滚动形式,它可以通过标记每个来获得相反,不同的子表达式并引用标签。关键子表达式将是:
A: x < n? goto B: Q
B: x = x + 1, goto A
这里的子表达式标签是“A:”和“B:”,而对子表达式的引用则标记为“goto A”和“goto B”,分别。因此,神奇的是,不定性的本质直接从无穷 lambda 演算中显现出来,无需单独或重新提出。
这种查看事物的方式甚至适用于二进制文件级别。每个字节的每种解释(无论它是开始 0、1、2 或更多个字节的指令的操作码的一部分,还是作为数据结构的一部分)都可以被视为串联存在,因此二进制文件文件是一个更大的通用展开结构的卷起,其物理字节码表示与其自身广泛重叠。
因此,当微积分扩展到包含无限项时,命令式编程语言范式自动从纯 lambda 微积分本身中出现。控制流结构直接体现在无限表达式本身的结构中;因此不需要额外的 hack(如 Landin 或后来的后代,如 monad)——因为它已经存在了。
这种命令式和函数式范式的综合出现于 1980 年代末,通过 USENET,但尚未发布。其中一部分已经隐含在对像 Prolog-II 这样的语言的处理中(大约在同一时间),以及 Irene Guessarian LNCS 99“代数语义”中通过无限表达式对循环递归结构的更早的处理。
现在,我之前说过基于岩浆的公式可能会让你到达相同的地方,或者近似的地方。我相信存在某种通用表示定理,该定理断言基于无限的公式提供了纯句法表示,并且通过此从基于单子的表示因素产生的语义为“基于 monad 的语义”=“无限 lambda 演算”+“无限语言的语义”。
同样,我们可以将上面的“Q”表达式视为延续;因此,可能还存在一个连续语义的通用表示定理,它类似地将这个公式回滚到无穷 lambda 演算中。
在这一点上,我还没有提到非理性无限项(即拥有无限数量的不同子项并且没有有限最小滚动的无限项)——特别是与过程间控制流语义相关的。有理项足以解释循环和分支,从而为过程内控制流语义提供平台;但对于作为过程间控制流语义的基本核心元素的调用返回语义来说,情况就不那么重要了,如果您认为子程序直接表示为经过修饰、美化的宏。
对于无限术语语言,可能存在类似于乔姆斯基层次结构的东西;因此,类型 3 对应于有理项,类型 2 对应于“代数项”(那些可以汇总到“转到”引用和“宏”定义的有限集合中的项),类型 0 对应于“先验项”。对我来说,这也是一个未解决的悬而未决的问题。
"Pure functional programming languages do not allow mutable data" ... actually it does, you just simply have to recognize where it lies hidden and see it for what it is.
Mutability is where two things have the same name and mutually exclusive times of existence so that they may be treated as "the same thing at different times". But as every Zen philosopher knows, there is no such thing as "same thing at different times". Everything ceases to exist in an instant and is inherited by its successor in possibly changed form, in a (possibly) uncountably-infinite succession of instants.
In the lambda calculus, mutability thus takes the form illustrated by the following example: (λx (λx f(x)) (x+1)) (x+1), which may also be rendered as "let x = x + 1 in let x = x + 1 in f(x)" or just "x = x + 1, x = x + 1, f(x)" in a more C-like notation.
In other words, "name clash" of the "lambda calculus" is actually "update" of imperative programming, in disguise. They are one and the same - in the eyes of the Zen (who is always right).
So, let's refer to each instant and state of the variable as the Zen Scope of an object. One ordinary scope with a mutable object equals many Zen Scopes with constant, unmutable objects that either get initialized if they are the first, or inherit from their predecessor if they are not.
When people say "mutability" they're misidentifying and confusing the issue. Mutability (as we've just seen here) is a complete red herring. What they actually mean (even unbeknonwst to themselves) is infinite mutability; i.e. the kind which occurs in cyclic control flow structures. In other words, what they're actually referring to - as being specifically "imperative" and not "functional" - is not mutability at all, but cyclic control flow structures along with the infinite nesting of Zen Scopes that this entails.
The key feature that lies absent in the lambda calculus is, thus, seen not as something that may be remedied by the inclusion of an overwrought and overthought "solution" like monads (though that doesn't exclude the possibility of it getting the job done) but as infinitary terms.
A control flow structure is the wrapping of an unwrapped (possibility infinite) decision tree structure. Branches may re-converge. In the corresponding unwrapped structure, they appear as replicated, but separate, branches or subtrees. Goto's are direct links to subtrees. A goto or branch that back-branches to an earlier part of a control flow structure (the very genesis of the "cycling" of a cyclic control flow structure) is a link to an identically-shaped copy of the entire structure being linked to. Corresponding to each structure is its Universally Unrolled decision tree.
More precisely, we may think of a control-flow structure as a statement that precedes an actual expression that conditions the value of that expression. The archetypical case in point is Landin's original case, itself (in his 1960's paper, where he tried to lambda-ize imperative languages): let x = 1 in f(x). The "x = 1" part is the statement, the "f(x)" is the value being conditioned by the statement. In C-like form, we could write this as x = 1, f(x).
More generally, corresponding to each statement S and expression Q is an expression S[Q] which represents the result Q after S is applied. Thus, (x = 1)[f(x)] is just λx f(x) (x + 1). The S wraps around the Q. If S contains cyclic control flow structures, the wrapping will be infinitary.
When Landin tried to work out this strategy, he hit a hard wall when he got to the while loop and went "Oops. Never mind." and fell back into what become an overwrought and overthought solution, while this simple (and in retrospect, obvious) answer eluded his notice.
A while loop "while (x < n) x = x + 1;" - which has the "infinite mutability" mentioned above, may itself be treated as an infinitary wrapper, "if (x < n) { x = x + 1; if (x < 1) { x = x + 1; if (x < 1) { x = x + 1; ... } } }". So, when it wraps around an expression Q, the result is (in C-like notation) "x < n? (x = x + 1, x < n? (x = x + 1, x < n? (x = x + 1, ...): Q): Q): Q", which may be directly rendered in lambda form as "x < n? (λx x < n (λx x < n? (λx·...) (x + 1): Q) (x + 1): Q) (x + 1): Q". This shows directly the connection between cyclicity and infinitariness.
This is an infinitary expression that, despite being infinite, has only a finite number of distinct subexpressions. Just as we can think of there being a Universally Unrolled form to this expression - which is similar to what's shown above (an infinite decision tree) - we can also think of there being a Maximally Rolled form, which could be obtained by labelling each of the distinct subexpressions and referring to the labels, instead. The key subexpressions would then be:
A: x < n? goto B: Q
B: x = x + 1, goto A
The subexpression labels, here, are "A:" and "B:", while the references to the subexpressions so labelled as "goto A" and "goto B", respectively. So, by magic, the very essence of Imperativitity emerges directly out of the infinitary lambda calculus, without any need to posit it separately or anew.
This way of viewing things applies even down to the level of binary files. Every interpretation of every byte (whether it be a part of an opcode of an instruction that starts 0, 1, 2 or more bytes back, or as part of a data structure) can be treated as being there in tandem, so that the binary file is a rolling up of a much larger universally unrolled structure whose physical byte code representation overlaps extensively with itself.
Thus, emerges the imperative programming language paradigm automatically out of the pure lambda calculus, itself, when the calculus is extended to include infinitary terms. The control flow structure is directly embodied in the very structure of the infinitary expression, itself; and thus requires no additional hacks (like Landin's or later descendants, like monads) - as it's already there.
This synthesis of the imperative and functional paradigms arose in the late 1980's via the USENET, but has not (yet) been published. Part of it was already implicit in the treatment (dating from around the same time) given to languages, like Prolog-II, and the much earlier treatment of cyclic recursive structures by infinitary expressions by Irene Guessarian LNCS 99 "Algebraic Semantics".
Now, earlier I said that the magma-based formulation might get you to the same place, or to an approximation thereof. I believe there is a kind of universal representation theorem of some sort, which asserts that the infinitary based formulation provides a purely syntactic representation, and that the semantics that arise from the monad-based representation factors through this as "monad-based semantics" = "infinitary lambda calculus" + "semantics of infinitary languages".
Likewise, we may think of the "Q" expressions above as being continuations; so there may also be a universal representation theorem for continuation semantics, which similarly rolls this formulation back into the infinitary lambda calculus.
At this point, I've said nothing about non-rational infinitary terms (i.e. infinitary terms which possess an infinite number of distinct subterms and no finite Minimal Rolling) - particularly in relation to interprocedural control flow semantics. Rational terms suffice to account for loops and branches, and so provide a platform for intraprocedural control flow semantics; but not as much so for the call-return semantics that are the essential core element of interprocedural control flow semantics, if you consider subprograms to be directly represented as embellished, glorified macros.
There may be something similar to the Chomsky hierarchy for infinitary term languages; so that type 3 corresponds to rational terms, type 2 to "algebraic terms" (those that can be rolled up into a finite set of "goto" references and "macro" definitions), and type 0 for "transcendental terms". That is, for me, an unresolved loose end, as well.