函数式编程语言如何工作?
如果函数式编程语言不能保存任何状态,他们如何做一些简单的事情,比如读取用户的input? 他们如何“存储”input(或为此存储任何数据?)
例如:如何将这个简单的C的东西转换成像Haskell这样的函数式编程语言?
#include<stdio.h> int main() { int no; scanf("%d",&no); return 0; }
(我的问题受到了这个出色的文章的启发: “名词王国的执行” ,阅读它让我更好地理解了什么是面向对象的编程,Java如何以一种极端的方式实现它,以及函数式编程语言是如何对比。)
如果函数式编程语言不能保存任何状态,他们如何做一些简单的事情,比如从用户那里读取input(我的意思是他们如何“存储”),或者存储任何数据?
正如你所收集的,函数式编程没有状态 – 但这并不意味着它不能存储数据。 不同的是,如果我写一个(哈斯克尔)声明沿线
let x = func value 3.14 20 "random" in ...
我保证x
的值在...
总是一样的:没有什么可以改变的。 同样,如果我有一个函数f :: String -> Integer
(一个函数接受一个string并返回一个整数),我可以肯定f
不会修改它的参数,或者更改任何全局variables,或者将数据写入文件, 等等。 正如sepp2k在上面的评论中所说的那样,这个非可变性对于推理程序是非常有帮助的:你编写的函数可以折叠,旋转和破坏你的数据,返回新的副本,这样你就可以将它们链接起来,你可以肯定没有这些函数调用可以做任何“有害”的事情。 你知道x
总是x
,并且你不必担心有人在x := foo bar
的声明和它的使用之间写了x := foo bar
,因为这是不可能的。
现在,如果我想读取用户的input呢? 正如Kenny所说的,这个想法是,一个不纯的函数是一个纯粹的函数,它以整个世界作为parameter passing,并返回结果和世界。 当然,你不想这样做:一方面,这是可怕的笨重,另一方面,如果我重复使用同一个世界对象会发生什么? 所以这个被抽象出来。 Haskell用IOtypes处理它:
main :: IO () main = do str <- getLine let no = fst . head $ reads str :: Integer ...
这告诉我们main
是一个IO操作,它什么都不返回; 执行这个动作就是运行一个Haskell程序的意思。 规则是IOtypes不能逃避IO操作; 在这方面,我们介绍一下使用do
动作。 因此, getLine
返回一个IO String
,这可以被认为有两种方式:第一,作为一个行动,当运行,产生一个string; 其次,它是一个被“污染”的string,因为它被不纯的获得。 第一个更正确,但第二个更有帮助。 <-
从IO String
String
取出IO String
并将其存储在str
– 但是由于我们处于IO操作中,所以我们必须将其包装起来,因此它不能“转义”。 下一行尝试读取整数( reads
)并抓取第一个成功的匹配( fst . head
); 这是纯粹的(没有IO),所以我们给它一个名称let no = ...
然后,我们可以在...
使用no
和str
。 因此我们存储了不纯的数据(从getLine
到str
)和纯数据( let no = ...
)。
这种使用IO的机制非常强大:它可以让你将程序的纯粹的algorithm部分从不纯的用户交互方面分离出来,并且在types级别执行。 您的minimumSpanningTree
函数不可能在代码中的其他位置更改某个地方,或者向用户写入消息等等。 它是安全的。
这就是在Haskell中使用IO所需要知道的一切; 如果这就是你想要的,你可以在这里停下来。 但是,如果你想明白为什么这个工作,请继续阅读。 (请注意,这些东西将特定于Haskell,其他语言可能会select不同的实现。)
所以这可能看起来像是一个骗子,不知何故增加纯粹的哈斯克尔杂质。 但事实并非如此 – 事实certificate,我们可以完全在纯Haskell中实现IOtypes(只要我们获得了RealWorld
)。 这个想法是这样的:IO操作IO type
与RealWorld -> (type, RealWorld)
函数相同,该函数采用现实世界并返回typestype
的对象和修改的RealWorld
。 然后,我们定义一些function,以便我们可以使用这种types而不会疯狂:
return :: a -> IO a return a = \rw -> (a,rw) (>>=) :: IO a -> (a -> IO b) -> IO b ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
第一个允许我们谈论IO操作,它不做任何事情: return 3
是一个IO操作,它不查询真实世界,只返回3
。 >>=
操作符,发音为“bind”,允许我们运行IO操作。 它从IO操作中提取值,通过函数传递它和现实世界,并返回生成的IO操作。 请注意, >>=
强制执行我们的规则,即IO操作的结果永远不被允许转义。
然后,我们可以将上面的main
转换为以下普通的function应用程序:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
Haskell运行时启动main
与最初的RealWorld
,我们设置! 一切都是纯粹的,它只是一个花哨的语法。
[ 编辑: 正如@Conal指出的 ,这实际上并不是Haskell用来做IO的东西。 如果添加并发性,或者在IO操作的中间改变世界,这个模型就会中断,所以Haskell不可能使用这个模型。 仅用于顺序计算是准确的。 因此,Haskell的IO可能有点闪避; 即使不是这样,也不是那么高雅。 Per @ Conal的观察,看看西蒙·佩顿 – 琼斯(Simon Peyton-Jones)在解决尴尬球队(pdf)第3.1节中所说的话; 他提出了什么可能相当于一个替代模式沿着这些线,但然后放弃它的复杂性,并采取不同的方式。
再一次,这解释了(几乎)IO和一般的可变性在Haskell中是如何工作的; 如果这是你想知道的一切,你可以在这里停止阅读。 如果你想要最后一剂理论,请继续阅读 – 但请记住,在这一点上,我们离你的问题已经很远了!
所以最后一件事情:原来这个结构 – return
和>>=
– 的参数types是非常普遍的; 它被称为monad,并且符号, return
和>>=
与其中的任何一个一起工作。 正如你在这里看到的,单子不是神奇的; 所有这一切都是神奇的, do
块do
function调用。 RealWorld
types是我们看到任何魔法的唯一地方。 像列表构造函数这样的types也是单子,它们与不纯代码无关。
你现在几乎都知道单子的概念(除了一些必须满足的规律和正式的math定义之外),但是你缺乏直觉。 网上有一些荒谬的monad教程, 我喜欢这个 ,但你有select。 但是, 这可能不会帮助你 ; 获得直觉的唯一真正方法是通过使用它们并在适当的时间阅读一对夫妇教程的组合。
然而, 你不需要那个直觉来理解IO 。 理解monad的全面概括是锦上添花,但你现在可以使用IO。 我向您展示了第一个main
function之后,您可以使用它。 你甚至可以把IO代码看作是不纯的语言! 但请记住,有一个潜在的function表示:没有人在作弊。
(PS:对不起,我走了很远。)
这里有很多好的答案,但是他们很长。 我将尝试给出一个有用的简短答案:
-
函数式语言将状态放置在C所在的相同位置:在已命名的variables中和在堆中分配的对象中。 不同之处在于:
-
在函数式语言中,“variables”在进入作用域时(通过函数调用或let-binding)获取其初始值,并且该值在之后不会改变 。 类似地,在堆上分配的对象立即用其所有字段的值进行初始化,其后不会改变。
-
“状态变化”不是通过改变现有variables或对象,而是通过绑定新variables或分配新对象来处理。
-
-
IO的作品是一个窍门。 产生一个string的副作用计算由一个函数来描述,该函数以一个世界作为参数,并返回一个包含该string和一个新世界的对。 世界包括所有磁盘驱动器的内容,每个发送或接收的networking数据包的历史logging,屏幕上每个像素的颜色,以及类似的东西。 这个诀窍的关键在于进入这个世界的时间是被严格限制的
-
没有一个程序可以制作一个世界的副本(你会把它放在哪里?)
-
没有任何计划可以抛弃世界
使用这个技巧,就有可能形成一个独特的世界,这个世界的状态随着时间的推移而演变。 没有用function语言编写的语言运行时系统通过更新独特的世界来实现副作用计算,而不是返回一个新的世界。
西蒙·佩顿·琼斯(Simon Peyton Jones)和菲尔·沃德(Phil Wadler)在他们的里程碑式的“势在必行的程序devise”(Imperative Functional Programming)中精彩地解释了这个技巧
-
我正在评论回复一个新的答案,给更多的空间:
我写了:
据我所知,这个
IO
故事(World -> (a,World)
)应用于Haskell是一个神话,因为该模型只解释纯粹的顺序计算,而Haskell的IO
types包括并发性。 通过“纯粹的顺序”,我的意思是即使是世界(宇宙)也不允许在命令式计算的开始和结束之间改变,而不是由于该计算。 举个例子,当你的电脑匆匆离开,你的大脑等不能。 并发性可以通过更类似World -> PowerSet [(a,World)]
,这允许非确定性和交错。
诺曼写道:
@Conal:我认为IO的故事很好地概括了非确定性和交错; 如果我没有记错的话,在“Awkward Squad”的文章中有一个很好的解释。 但是我不清楚一篇很好的论文能够清楚地解释真实的并行性。
@Norman:泛化在什么意义上? 我build议通常给出的指称模型/解释World -> (a,World)
与Haskell IO
不匹配,因为它没有考虑非确定性和并发性。 可能会有一个更复杂的模型,比如World -> PowerSet [(a,World)]
,但是我不知道这个模型是否已经被计算出来,并且显示出足够的一致性。 我个人怀疑这样的野兽可以被发现,因为IO
是由成千上万的FFI导入的命令式API调用填充的。 因此, IO
正在实现其目的:
开放的问题:
IO
monad已经成为Haskell的罪犯。 (当我们什么都不懂的时候,我们把它扔到IO monad中。)
(来自Simon PJ的POPL谈话穿着发衬衫穿着发衬衫:对Haskell的回顾 。)
Simon在“ 解决Awkward Squad”第3.1节中指出了什么对于type IO a = World -> (a, World)
是无效的,包括“当我们添加并发时,这种方法不能很好地扩展”。 然后,他提出了一种可能的替代模式,然后放弃了对指称性解释的尝试
但是,我们将采用一种操作语义,基于对过程算术语义的标准方法。
没有find一个精确而有用的指示模型,这就是为什么我看到Haskell IO背离了我们所谓的“函数式编程”的精神和深远的好处,或者Peter Landin更具体地命名为“外延式编程” 。 在这里看到评论。
函数式编程源自lambda微积分。 如果你真的想了解function编程,请查看http://worrydream.com/AlligatorEggs/
学习lambda微积分是一种“有趣”的方式,将您带入函数式编程的激动人心的世界!
如何知道Lambda微积分有助于函数式编程。
所以Lambda微积分是许多现实世界编程语言的基础,例如Lisp,Scheme,ML,Haskell,….
假设我们想要描述一个函数,将三个input添加到任何input中,我们将这样写:
plus3 x = succ(succ(succ x))
阅读“plus3是一个函数,当适用于任何数字x,产生x的继任者的继任者的继任者”
请注意,将任意数字加3的函数不必命名为plus3; 名称“plus3”只是命名这个函数的一个简便的简写
( plus3 x) (succ 0) ≡ ((λ x. (succ (succ (succ x)))) (succ 0))
注意,我们使用lambda符号作为函数(我认为它看起来有点像鳄鱼我猜这就是鳄鱼卵的想法来自哪里)
lambda符号是鳄鱼 (一个函数),x是它的颜色。 你也可以把x作为一个参数(Lambda微积分函数实际上只假设有一个参数),其余的你可以把它看作函数的主体。
现在考虑抽象:
g ≡ λ f. (f (f (succ 0)))
参数f用于函数位置(在调用中)。 我们称之为ga高阶函数,因为它将另一个函数作为input。 你可以把其他函数f想象成“ 鸡蛋 ”。 现在,我们已经创build了两个函数或“ 鳄鱼 ”,我们可以做这样的事情:
(g plus3) = (λ f. (f (f (succ 0)))(λ x . (succ (succ (succ x)))) = ((λ x. (succ (succ (succ x)))((λ x. (succ (succ (succ x)))) (succ 0))) = ((λ x. (succ (succ (succ x)))) (succ (succ (succ (succ 0))))) = (succ (succ (succ (succ (succ (succ (succ 0)))))))
如果您注意到您可以看到我们的λ鳄鱼吃了我们的λx鳄鱼,然后吃λx鳄鱼并死亡。 然后我们的λx鳄鱼重生在λf的鳄鱼卵中。 然后重复这个过程,左边的λx鳄鱼现在吃右边的另一个λx鳄鱼。
然后你可以使用这个简单的“ Alligators ”规则来吃 “ Alligators ”来devise一个语法,从而函数式编程语言诞生了!
所以你可以看看你是否知道Lambda微积分,你将会明白Functional语言是如何工作的。
在Haskell中处理状态的技术非常简单。 而且你不需要理解单子就能掌握它。
在具有状态的编程语言中,通常有一些值存储在某个地方,某些代码会执行,然后存储新的值。 在命令式语言中,这个状态只是“在后台”的某个地方。 在(纯粹的)函数式语言中,您明确地表示了这一点,所以您明确地写出了转换状态的函数。
因此,不是写一些Xtypes的状态,而是编写将X映射到X的函数。就是这样! 你从思考状态转换到想要在状态上执行什么操作。 然后,您可以将这些function链接在一起,并以各种方式将它们组合在一起制作整个节目。 当然,您不仅限于将X映射到X.您可以编写函数,将各种数据组合作为input,并在最后返回各种组合。
单子是帮助组织这一点的工具之一。 但monad实际上并不是解决问题的办法。 解决办法是考虑状态转换而不是状态。
这也适用于I / O。 实际上发生的是这样的:而不是从用户那里得到一些与scanf
直接等价的input,并将其存储在某个地方,而是写一个函数来说明如果你有了scanf
的结果,你会怎么做,然后将该函数传递给I / O API。 这正是当你在Haskell中使用IO
monad时所做的。 所以你永远不需要在任何地方存储任何I / O的结果 – 你只需要编写代码来说明你想如何转换它。
(一些function语言允许不纯的function。)
对于纯粹的function语言,真实世界的交互通常被包含为函数参数之一,如下所示:
RealWorld pureScanf(RealWorld world, const char* format, ...);
不同的语言有不同的策略将世界从程序员中抽离出来。 例如,Haskell使用monad来隐藏world
论点。
但是函数式语言本身的纯粹部分已经是图灵完备的,这意味着在C中可行的任何东西在Haskell中也是可行的。 与命令式语言的主要区别在于不是修改状态:
int compute_sum_of_squares (int min, int max) { int result = 0; for (int i = min; i < max; ++ i) result += i * i; // modify "result" in place return result; }
将修改部分并入函数调用中,通常将循环转换为recursion:
int compute_sum_of_squares (int min, int max) { if (min >= max) return 0; else return min * min + compute_sum_of_squares(min + 1, max); }
function语言可以保存状态! 他们通常只是鼓励或强迫你明确这样做。
例如,查看Haskell的State Monad 。
函数编程可能是有用的, 我们其余的人
哈斯克尔:
main = do no <- readLn print (no + 1)
你当然可以用函数式语言给variables赋值。 你不能改变它们(所以基本上所有的variables都是函数式语言中的常量)。