Eri*_*ono 26 f# haskell functional-programming scala
我已经潜入函数式编程超过3年了,我一直在阅读和理解函数式编程的许多文章和方面.
但我经常偶然发现许多关于副作用计算中"世界"的文章,以及在IO monad样本中携带和复制"世界"的文章.在这种情况下,"世界"意味着什么?这在所有副作用计算环境中是否与"世界"相同,还是仅在IO monads中应用?
关于Haskell的文档和其他文章也多次提到"世界".
关于这个"世界"的一些参考:http: //channel9.msdn.com/Shows/Going+Deep/Erik-Meijer-Functional-Programming
这个:http: //www.infoq.com/presentations/Taming-Effect-Simon-Peyton-Jones
我期待一个样本,而不仅仅是对世界概念的解释.我欢迎Haskell,F#,Scala,Scheme中的示例代码.
And*_*erg 50
"世界"只是一个捕捉"世界状态"的抽象概念,即当前计算之外的一切状态.
拿这个I/O功能,例如:
write : Filename -> String -> ()
Run Code Online (Sandbox Code Playgroud)
这是无效的,因为它会通过副作用更改文件(其内容是世界状态的一部分).但是,如果我们将世界建模为显式对象,我们可以提供此功能:
write : World -> Filename -> String -> World
Run Code Online (Sandbox Code Playgroud)
这将获取当前世界并在功能上生成一个"新"文件,并修改文件,然后您可以将其传递给连续调用.世界本身只是一种抽象类型,除了通过相应的函数之外,没有办法直接窥视它read.
现在,上述界面存在一个问题:没有进一步的限制,它将允许程序"复制"世界.例如:
w1 = write w "file" "yes"
w2 = write w "file" "no"
Run Code Online (Sandbox Code Playgroud)
你曾w两次使用同一个世界,产生两个不同的未来世界.显然,作为物理I/O的模型,这没有任何意义.为了防止这样的例子,需要一种更加花哨的类型系统,以确保线性处理世界,即从未使用过两次.Clean的语言基于这种想法的变化.
或者,您可以封装世界,使其永远不会变得明确,从而无法通过构造进行复制.这就是I/O monad所实现的 - 它可以被认为是一个状态monad,其状态是世界,它隐含地通过monadic动作.
Lui*_*las 12
"世界"是一种将命令式编程嵌入到纯函数式语言中的概念.
正如您当然所知,纯函数式编程要求函数的结果完全依赖于参数的值.因此,假设我们想将典型getLine操作表达为纯函数.有两个明显的问题:
getLine 每次用相同的参数调用时都会产生不同的结果(在本例中没有参数).getLine消耗一部分流的副作用.如果您的程序使用getLine,则(a)每次调用它必须使用输入的不同部分,(b)程序输入的每个部分必须由某些调用使用.(getLine除非该输入行在输入中出现两次,否则您不能两次调用两次相同的输入行;您不能让程序随机跳过一行输入.)所以getLine只是不能成为一个功能吧?嗯,不是那么快,我们可以做一些技巧:
getLine可以返回不同的结果.为了使其与纯粹的功能行为兼容,这意味着纯粹的功能getLine可能会引发争论:getLine :: W -> String.然后我们可以通过规定每个调用必须使用不同的W参数值来调和每个调用的不同结果的想法.您可以想象,它W代表输入流的状态.getLine必须以某种确定的顺序执行多次调用,并且每次调用必须消耗前一次调用遗留的输入.更改:给出getLine类型W -> (String, W),并禁止程序W多次使用一个值(我们可以在编译时检查).现在要getLine在程序中多次使用,您必须注意将先前调用的W结果提供给后续调用.只要你能保证Ws不被重用,你就可以使用这种技术将任何(单线程)命令式程序转换成纯粹的功能性程序.您甚至不需要为该W类型提供任何实际的内存中对象- 您只需键入检查您的程序并对其进行分析以证明每个W只使用一次,然后发出不引用任何类型的代码.
因此,"世界"就是这个想法,但不仅仅是为了涵盖所有必要的操作getLine.
现在已经解释了所有这些,你可能想知道你是否更好地了解这一点.我的意见不是,你不是.看,IMO,整个"传递世界"的想法就像monad教程之类的东西,其中有太多的Haskell程序员选择以实际上没有的方式"有用".
"传递世界"通常作为"解释"来帮助新手理解Haskell IO.但问题在于:(a)对于许多人来说,这是一个非常奇特的概念("你的意思是我要通过整个世界的状态?"),(b)非常抽象(很多人无法理解你的程序几乎每个函数都会有一个未使用的虚拟参数,既不会出现在源代码中也不会出现在目标代码中),而且(c)不是最简单,最实用的解释.
Haskell I/O,恕我直言的最简单,最实用的解释如下:
getLine不能起作用的东西.getLine.这意味着那些东西不是一种功能.我们称之为行动.putStrLn :: String -> IO ()),将动作作为参数接受的函数(例如(>>) :: IO a -> IO b -> IO b),等等).execute :: IO a -> a因为它不是真正的功能.main :: IO ()根据子动作编写动作来编写可执行的Haskell程序.传递代表"世界"的值是在纯声明性编程中制作用于执行IO(和其他副作用)的纯模型的一种方法.
纯声明(不仅仅是功能)编程的"问题"是显而易见的.纯声明性编程提供了一种计算模型.这些模型可以表达任何可能的计算,但在现实世界中,我们使用程序让计算机做一些不是理论意义上的计算:输入,渲染到显示,读取和写入存储,使用网络,控制机器人等等您可以直接将所有这些程序建模为计算(例如,如果输入是计算,应该将哪个输出写入文件),但实际与程序外部事物的交互不是纯模型的一部分.
这也是命令式编程的真实情况.作为C编程语言的计算"模型"无法写入文件,从键盘读取或任何东西.但是命令式编程中的解决方案是微不足道的.在命令式模型中执行计算是执行指令序列,并且每个指令实际执行的操作取决于程序执行时的整个环境.因此,您只需提供执行IO操作的"魔术"指令即可.由于命令式程序员习惯于在操作上考虑他们的程序1,这非常适合他们已经在做的事情.
但是在所有纯粹的计算模型中,给定的计算单位(函数,谓词等)所做的只应取决于其输入,而不是每次都可能不同的任意环境.因此,不仅可以执行IO操作,而且还可以实现依赖于程序外的Universe的计算.
然而,解决方案的想法相当简单.您构建了一个模型,用于说明IO操作在整个纯计算模型中的工作方式.那么适用于纯模型的所有原理和理论也将适用于模拟IO的部分.然后,在语言或库实现中(因为它不能在语言本身中表达),您可以将IO模型的操作与实际的IO操作联系起来.
这使我们传递代表世界的价值.例如,Mercury中的"hello world"程序如下所示:
:- pred main(io::di, io::uo) is det.
main(InitialWorld, FinalWorld) :-
print("Hello world!", InitialWorld, TmpWorld),
nl(TmpWorld, FinalWorld).
Run Code Online (Sandbox Code Playgroud)
给出InitialWorld了程序,该类型的值io代表程序外的整个Universe.它将这个世界传递print给TmpWorld了世界,这世界就像是InitialWorld"你好世界!" 已被打印到终端,以及其他任何发生在此期间,因为InitialWorld传递到main也纳入.它然后传递TmpWorld给nl,回馈FinalWorld(一个非常喜欢的世界,TmpWorld但它包含了换行的印刷,加上同时发生的任何其他效果).FinalWorld是世界最终main退回操作系统的状态.
当然,我们并没有真正将整个宇宙作为一个价值传递给程序.在底层实现中,通常根本没有类型的值io,因为没有对实际传递有用的信息; 它都存在于程序之外.但是使用我们传递io值的模型允许我们编程,好像整个Universe是受其影响的每个操作的输入和输出(并因此看到任何不采用输入和输出io参数的操作都可以'牛逼受到外界的影响).
事实上,通常你甚至不会想到那些做IO的程序就好像它们在宇宙中传播一样.在真正的Mercury代码中,你使用"状态变量"语法糖,并像这样写上面的程序:
:- pred main(io::di, io::uo) is det.
main(!IO) :-
print("Hello world!", !IO),
nl(!IO).
Run Code Online (Sandbox Code Playgroud)
感叹号语法意味着!IO真正代表了两个参数,IO_X并且IO_Y,在X与Y部分由编译器自动使得状态变量是通过在它们的排列顺序的目标"螺纹"填写.这不仅仅适用于IO btw的上下文,状态变量在Mercury中是非常方便的语法糖.
因此,程序员实际上倾向于将此视为一系列步骤(取决于并影响外部状态),这些步骤按写入顺序执行.!IO几乎成为一个神奇的标记,只标记这适用的调用.
在Haskell中,IO的纯模型是monad,"hello world"程序如下所示:
main :: IO ()
main = putStrLn "Hello world!"
Run Code Online (Sandbox Code Playgroud)
解释IOmonad的一种方法与monad相似State; 它会自动穿过状态值,monad中的每个值都可以依赖或影响这个状态.只有在IO状态被线程化的情况下才是整个宇宙,就像Mercury程序一样.使用Mercury的状态变量和Haskell的表示法,这两种方法看起来非常相似,"world"以一种尊重源代码中调用的顺序自动穿过,但仍然IO明确地有动作标记.
正如在sacundim答案中所解释的那样,将 Haskell的IOmonad 解释为IO-y计算模型的另一种方法是想象putStrLn "Hello world!"实际上并不是"宇宙"需要线程化的计算,而是putStrLn "Hello World!"本身描述可以采取的IO操作的数据结构.基于这种理解,IOmonad中正在执行的程序是使用纯Haskell程序在运行时生成命令式程序.在纯Haskell中,没有办法实际执行该程序,但由于main类型IO () main本身是对这样的程序的评估,我们只是在操作上知道Haskell运行时将执行该main程序.
由于我们将这些纯粹的IO 模型与实际的外部交互联系起来,我们需要谨慎一点.我们编程好像整个宇宙是一个值,我们可以传递与其他值相同的值.但是其他值可以传递给多个不同的调用,存储在多态容器中,以及许多其他对实际Universe没有任何意义的事情.因此,我们需要一些限制,阻止我们对模型中的"世界"做任何事情,这与实际可以对现实世界做出的事情无关.
Mercury采用的方法是使用独特的模式来强制io值保持唯一.这就是为什么在输入和输出的世界被宣布为io::di与io::uo分别; 它是一个简写,用于声明第一个参数的类型,io它的模式是di("破坏性输入"的缩写),而第二个参数的类型是io,它的模式是uo("唯一输出"的缩写).由于io是抽象类型,因此无法构造新的类型,因此满足唯一性要求的唯一方法是始终将io值传递给最多一个调用,这也必须返回一个唯一io值,然后输出最后io一件事的最终价值.
在Haskell中采用的方法是使用monad接口来允许monad中的值IO由纯数据和其他IO值构造,但不会在IO值上公开任何函数,以允许您从IOmonad中"提取"纯数据.这意味着只有所IO包含的值main才能执行任何操作,并且这些操作必须正确排序.
我之前提到过,IO用纯语言编写的程序员仍倾向于在操作上思考他们的大多数IO.那么,如果我们只是按照命令式程序员的方式来思考它,为什么要为IO提出一个纯粹的模型呢?最大的优点是,现在所有适用于所有语言的理论/代码/适用于IO代码.
例如,在Mercury中,相当于fold逐个元素列出元素以构建累加器值,这意味着fold将一些任意类型的输入/输出变量作为累加器(这是Mercury中非常常见的模式)标准库,这就是为什么我说状态变量语法在其他上下文中经常被证明非常方便而不是IO).由于"世界"在Mercury程序中明确地显示为类型中的值io,因此可以使用io值作为累加器!在Mercury中打印字符串列表就像值一样简单.我们得到了许多"高阶"IO操作,这些IO操作必须在一种语言中重新实现IO,该语言通过一些完全特殊的机制来处理IO.foldl(print, MyStrings, !IO).类似地,在Haskell中,通用monad/functor代码可以正常工作IO
此外,由于我们避免通过IO破坏纯模型,即使在存在IO的情况下,对计算模型也适用的理论仍然是正确的.这使得程序员和程序分析工具的推理不必考虑是否可能涉及IO.例如,在Scala等语言中,即使很多"普通"代码实际上是纯粹的,但是对纯代码起作用的优化和实现技术通常也不适用,因为编译器必须假定每个调用都可能包含IO或其他影响.
1在程序操作上思考程序意味着在执行计算机时将执行的操作.