繁体   English   中英

“世界”在函数式编程世界中意味着什么?

[英]What does the “world” mean in functional programming world?

我已经潜入函数式编程超过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中的示例代码。

“世界”只是一个捕捉“世界状态”的抽象概念,即当前计算之外的一切状态。

拿这个I / O功能,例如:

write : Filename -> String -> ()

这是无效的,因为它会通过副作用更改文件(其内容是世界状态的一部分)。 但是,如果我们将世界建模为显式对象,我们可以提供此功能:

write : World -> Filename -> String -> World

这将获取当前世界并在功能上生成一个“新”文件,并修改文件,然后您可以将其传递给连续调用。 世界本身只是一种抽象类型,除了通过read类的相应函数之外,没有办法直接窥视它。

现在,上述界面存在一个问题:没有进一步的限制,它将允许程序“复制”世界。 例如:

w1 = write w "file" "yes"
w2 = write w "file" "no"

你已经使用了同一个世界w两次,产生两个不同的未来世界。 显然,作为物理I / O的模型,这没有任何意义。 为了防止这样的例子,需要一种更加花哨的类型系统,以确保线性处理世界,即从未使用过两次。 Clean的语言基于这种想法的变化。

或者,您可以封装世界,使其永远不会变得明确,从而无法通过构造进行复制。 这就是I / O monad所实现的 - 它可以被认为是一个状态monad,其状态是世界,它隐含地通过monadic动作。

“世界”是一种将命令式编程嵌入到纯函数式语言中的概念。

正如您当然所知,纯函数式编程要求函数的结果完全依赖于参数的值。 因此,假设我们想将典型的getLine操作表示为纯函数。 有两个明显的问题:

  1. 每次使用相同的参数调用getLine都会产生不同的结果(在本例中没有参数)。
  2. getLine具有消耗流的某些部分的副作用。 如果您的程序使用getLine ,则(a)每次调用它必须使用输入的不同部分,(b)程序输入的每个部分必须由某些调用使用。 (你不能两次调用getLine读取相同的输入行两次,除非该行在输入中出现两次;你不能让程序随机跳过一行输入。)

所以getLine只是不能成为一个函数,对吧? 嗯,不是那么快,我们可以做一些技巧:

  1. 多次调用getLine可以返回不同的结果。 为了使其与纯函数行为兼容,这意味着纯函数式getLine可以接受一个参数: getLine :: W -> String 然后我们可以通过规定每个调用必须使用W参数的不同值来调和每个调用的不同结果的概念。 您可以想象W代表输入流的状态。
  2. 必须以某种确定的顺序执行对getLine多次调用,并且每次调用必须消耗前一次调用遗留的输入。 更改:给getLine类型W -> (String, W) ,并禁止多次使用W值的程序(我们可以在编译时检查)。 现在要在程序中多次使用getLine ,您必须注意将先前调用的W结果提供给后续调用。

只要您可以保证W s不被重用,您就可以使用这种技术将任何(单线程)命令式程序转换为纯粹的功能性程序。 您甚至不需要为W类型提供任何实际的内存中对象 - 您只需键入 - 检查您的程序并进行分析以证明每个W仅使用一次,然后发出不涉及任何内容的代码那种。

因此,“世界”就是这个想法,但总体上涵盖了所有必要的操作,而不仅仅是getLine


现在已经解释了所有这些,你可能想知道你是否更好地了解这一点。 我的意见不是,你不是。 看,IMO,整个“传递世界”的想法就像monad教程之类的东西,其中有太多的Haskell程序员选择以实际上没有的方式“有用”。

“传递世界”通常作为“解释”来帮助新手理解Haskell IO。 但问题在于:(a)对于许多人来说,这是一个非常奇特的概念(“你的意思是我要通过整个世界的状态 ?”),(b)非常抽象(很多人无法理解你的程序几乎每个函数都会有一个未使用的虚拟参数,既不会出现在源代码中也不会出现在目标代码中),而且(c)不是最简单,最实用的解释。

Haskell I / O,恕我直言的最简单,最实用的解释如下:

  1. Haskell纯粹是功能性的,所以像getLine这样的东西不能成为函数。
  2. 但是Haskell有像getLine这样的东西。 这意味着那些东西不是一种功能。 我们称之为行动
  3. Haskell允许您将操作视为值。 您可以拥有产生动作的函数(例如, putStrLn :: String -> IO () ),接受动作作为参数的函数(例如, (>>) :: IO a -> IO b -> IO b)等。
  4. 然而,Haskell没有执行动作的功能。 不能有execute :: IO a -> a因为它不是真正的函数。
  5. 哈斯克尔有内置的功能来撰写动作:使复合动作进行简单的操作。 使用基本操作和动作组合器,您可以将任何命令性程序描述为操作。
  6. Haskell编译器知道如何将操作转换为可执行的本机代码。 因此,您通过根据子动作编写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).

该程序被赋予InitialWorld ,类型为io的值,代表程序外的整个Universe。 它将这个世界传递给了print ,它将它回馈给了TmpWorld ,这个世界就像是InitialWorld但是“Hello world!” 已被打印到终端,并且在此期间发生的任何其他事件,因为InitialWorld被传递到main也被合并。 然后TmpWorld传递给nl ,它返回了FinalWorld (一个非常像TmpWorld的世界,但它结合了换行符的打印,以及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).

感叹号语法表示!IO实际上代表两个参数IO_XIO_Y ,其中XY部分由编译器自动填充,使得状态变量按照它们的顺序“穿线”目标。书面。 这不仅仅适用于IO btw的上下文,状态变量在Mercury中是非常方便的语法糖。

因此,程序员实际上倾向于将此视为一系列步骤(取决于并影响外部状态),这些步骤按写入顺序执行。 !IO几乎成为一个神奇的标记,只标记这适用的调用。

在Haskell中,IO的纯模型是monad,“hello world”程序如下所示:

main :: IO ()
main = putStrLn "Hello world!"

解释IO monad的一种方法类似于State monad; 它会自动穿过状态值,monad中的每个值都可以依赖或影响这个状态。 只有在IO的情况下,被线程化的状态才是整个Universe,就像Mercury程序一样。 使用Mercury的状态变量和Haskell的表示法,这两种方法看起来非常相似,“world”以一种尊重源代码中调用的顺序的方式自动穿过,但仍然有IO动作明确标记。

正如在sacundim的回答中所解释的那样, Haskell的IO monad 解释为IO-y计算模型的另一种方法是想象putStrLn "Hello world!" 实际上并不是一个计算“宇宙”需要被线程化的计算,而是putStrLn "Hello World!" 它本身就是一个描述可以采取的IO动作的数据结构。 基于这种理解, IO monad中正在执行的程序是使用纯Haskell程序在运行时生成命令式程序。 在纯哈斯克尔没有办法实际执行该程序,但由于main是类型的IO () main本身求这样的计划,我们只知道操作性的Haskell的运行时将执行的main程序。

由于我们将这些纯粹的IO 模型与实际的外部交互联系起来,我们需要谨慎一点。 我们编程好像整个宇宙是一个值,我们可以传递与其他值相同的值。 但是其他值可以传递给多个不同的调用,存储在多态容器中,以及许多其他对实际Universe没有任何意义的事情。 因此,我们需要一些限制,阻止我们对模型中的“世界”做任何事情,这与实际可以对现实世界做出的事情无关。

Mercury采用的方法是使用独特的模式来强​​制io值保持唯一。 这就是输入和输出世界分别被声明为io::diio::uo ; 它是一个简写,用于声明第一个参数的类型是io ,它的模式是di (“破坏性输入”的缩写),而第二个参数的类型是io ,其模式是uo (“唯一输出”的缩写) 。 由于io是一个抽象类型,因此无法构造新的类型,因此满足唯一性要求的唯一方法是始终将io值传递给最多一个调用,这也必须返回一个唯一的io值,然后从你调用的最后一个东西输出最终的io值。

在Haskell中采用的方法是使用monad接口来允许IO monad中的值从纯数据和其他IO值构造,但不暴露IO值上的任何函数,这些函数允许您从中提取纯数据。 IO monad。 这意味着只有包含在mainIO值才能执行任何操作,并且这些操作必须正确排序。

之前我曾提到,用纯语言编写IO程序员仍倾向于在操作上思考他们的大多数IO。 那么,如果我们只是按照命令式程序员的方式来思考它,为什么要为IO提出一个纯粹的模型呢? 最大的优点是,现在所有适用于所有语言的理论/代码/适用于IO代码。

例如,在Mercury中, fold的等价物逐个元素地fold处理以构建累加器值,这意味着fold将一些任意类型的输入/输出变量对作为累加器(这是一个非常常见的模式) Mercury标准库,这就是为什么我说状态变量语法在其他情况下通常非常方便而不是IO)。 由于“世界”在Mercury程序中明确地显示为类型io的值,因此可以使用io值作为累加器! 在Mercury中打印字符串列表就像foldl(print, MyStrings, !IO)一样简单。 类似地,在Haskell中,通用monad / functor代码在IO值上工作得很好。 我们得到了许多“高阶”IO操作,这些IO操作必须在一种语言中重新实现IO,该语言通过一些完全特殊的机制来处理IO。

此外,由于我们避免通过IO破坏纯模型,即使在存在IO的情况下,对计算模型也适用的理论仍然是正确的。 这使得程序员和程序分析工具的推理不必考虑是否可能涉及IO。 例如,在Scala等语言中,即使很多“普通”代码实际上是纯粹的,但是对纯代码起作用的优化和实现技术通常也不适用,因为编译器必须假定每个调用都可能包含IO或其他影响。


1在程序操作上思考程序意味着在执行计算机时将执行的操作。

我想我们应该首先阅读关于这个主题的事情是解决尴尬的小队 (我没有这样做,我就后悔了。)作者实际上描述的GHC的内部表示IOworld -> (a,world)为“一个黑客位的”。 我认为这种“黑客”意味着一种无辜的谎言。 我认为这里有两种谎言:

  1. GHC假装“世界”可以通过某种变量来表示。
  2. 类型world -> (a,world)基本上说如果我们能以某种方式实例化世界,那么我们世界的“下一个状态”在功能上由计算机上运行的一些小程序决定。 由于这显然无法实现,原语(当然)实现为具有副作用的函数,忽略了无意义的“世界”参数,就像在大多数其他语言中一样。

作者在两个基础上为这个“黑客”辩护:

  1. 通过将IO视为类型world -> (a,world)的薄包装world -> (a,world) ,GHC可以为IO代码重用许多优化,因此这种设计非常实用且经济。
  2. 如果编译器满足某些属性,则可以证明如上实现的IO计算的操作语义是合理的。 引用本文作为证明。

问题(我想在这里问一下,但是你先问过它,原谅我在这里写)是在标准的'懒惰IO'函数的存在中,我不再确定GHC的操作语义是否仍然合理。

标准的“惰性IO”函数(如hGetContents内部调用unsafeInterleaveIO ,而后者又相当于单线程程序的unsafeDupableInterleaveIO

unsafeDupableInterleaveIO :: IO a -> IO a
unsafeDupableInterleaveIO (IO m)
     = IO ( \ s -> let  r = case m s of (# _, res #) -> res
                   in  (# s, r #))

假设等式推理仍然适用于这种程序(注意m是一个不纯的函数)并忽略构造函数,我们有unsafeDupableInterleaveIO m >>= f ==> \\world -> f (snd (m world)) world从语义上讲,它与安德烈亚斯罗斯伯格描述的效果相同:它“重复”世界。 由于我们的世界不能以这种方式复制,并且Haskell程序的精确评估顺序几乎是不可预测的 - 我们得到的是一些几乎不受约束和不同步的并发竞争,用于某些宝贵的系统资源,如文件句柄。 当然, Ariola&Sabry从未考虑过这种操作。 所以我在这方面不同意安德烈亚斯 - 即使我们将自己限制在标准库的限制范围内 ,IO monad也没有真正正确地处理世界(这就是为什么有些人说懒惰的IO很糟糕)。

世界意味着 - 物理的,现实的世界。 (请注意,只有一个。)

通过忽略限制在CPU和内存中的物理过程,可以对每个函数进行分类:

  1. 那些在物理世界中没有影响的东西(除了短暂的,在CPU和RAM中几乎不可观察的影响)
  2. 那些确实有可观察到的影响。 例如:在打印机上打印一些东西,通过网络电缆发送电子,发射火箭或移动磁头。

区别是有点人为的,因为在现实运行即使是最纯粹的Haskell程序也会产生可观察到的影响,例如:你的CPU变热,导致风扇打开。

基本上你编写的每个程序都可以分为两部分(在FP中,在命令式/ OO世界中没有这样的区别)。

  1. 核心/纯部分:这是您应用程序的实际逻辑/算法,用于解决构建应用程序的问题。 (95%的应用程序今天缺少这一部分,因为它们只是乱七八糟的API调用,并且人们开始称自己为程序员)例如:在图像处理工具中,对图像应用各种效果的算法属于这个核心部分。 所以在FP中,你使用像纯度等FP这样的概念来构建这个核心部分。你可以构建你的函数来获取输入和返回结果,并且在你的应用程序的这一部分中没有任何突变。

  2. 外层部分:现在让我们说你已经完成了图像处理工具的核心部分,并通过使用各种输入调用函数并检查输出来测试算法,但这不是你可以发布的东西,用户应该如何使用这个核心部分,没有面子,它只是一堆功能。 现在要从最终用户的角度使这个核心usable ,你需要构建某种UI,从磁盘读取文件的方式,可能使用一些嵌入式数据库来存储用户首选项,列表继续。 这种与其他各种东西的交互,这不是你的应用程序的核心概念,但仍然需要使它可用,在FP中被称为world

练习:考虑一下你之前构建的任何应用程序,并尝试将其分为上面提到的两个部分,希望这会使事情变得更清晰。

例如,世界指的是与现实世界的互动/具有副作用

fprintf file "hello world"

这有副作用 - 该文件已添加"hello world"

这与纯粹的功能代码相反

let add a b = a + b

没有副作用

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM