除了Monads之外,还有哪些其他方式可以用纯函数语言处理?

时间:2012-11-23 23:15:48

标签: haskell functional-programming mercury

所以我开始围绕Monads(在Haskell中使用)。我很好奇IO或状态可以用纯函数语言处理的其他方式(理论上或现实中)。例如,有一种名为“mercury”的逻辑语言使用“效果打字”。在诸如haskell之类的程序中,效果打字工作会如何?其他系统如何运作?

7 个答案:

答案 0 :(得分:76)

这里涉及几个不同的问题。

首先,IOState是完全不同的事情。 State很容易做到 你自己:只需要为每个函数传递一个额外的参数,然后再返回一个 结果,你有一个"有状态功能&#34 ;;例如,将a -> b转换为 a -> s -> (b,s)

这里没有任何魔法:Control.Monad.State提供了一个包装器 使用"状态行动" s -> (a,s)形式也很方便 作为一堆帮助函数,但就是这样。

从本质上讲,I / O必须在实现方面有一些魔力。但是这里有 很多在Haskell中表达I / O的方法都没有涉及单词" monad"。 如果我们原样有一个无IO的Haskell子集,我们想从中发明IO 划伤,不知道任何关于monads的事情,我们可能会有很多事情 做。

例如,如果我们要做的就是打印到stdout,我们可能会说:

type PrintOnlyIO = String

main :: PrintOnlyIO
main = "Hello world!"

然后有一个RTS(运行时系统)来评估字符串并打印它。 这让我们可以编写任何I / O完全由打印组成的Haskell程序 到stdout。

然而,这并不是非常有用,因为我们想要互动!所以让我们发明吧 允许它的新型IO。想到的最简单的事情是

type InteractIO = String -> String

main :: InteractIO
main = map toUpper

这种IO方法允许我们编写从stdin读取并写入的任何代码 stdout(Prelude附带一个函数interact :: InteractIO -> IO () 顺便说一句,这样做。

这要好得多,因为它可以让我们编写交互式程序。但是它 与我们想要做的所有IO相比仍然非常有限,而且相当 容易出错(如果我们不小心尝试读入stdin,程序太过分了 将阻止,直到用户输入更多内容。

我们希望能够做的不仅仅是读取标准输入并写入标准输出。这是怎么回事 早期版本的Haskell做了I / O,大约是:

data Request = PutStrLn String | GetLine | Exit | ...
data Response = Success | Str String | ...
type DialogueIO = [Response] -> [Request]

main :: DialogueIO
main resps1 =
    PutStrLn "what's your name?"
  : GetLine
  : case resps1 of
        Success : Str name : resps2 ->
            PutStrLn ("hi " ++ name ++ "!")
          : Exit

当我们写main时,我们得到一个惰性列表参数并返回一个惰性列表作为 结果。我们返回的惰性列表的值为PutStrLn sGetLine; 在我们产生(请求)值之后,我们可以检查下一个元素 (响应)列表,RTS将安排它作为对我们的响应 请求。

有很多方法可以更好地使用这种机制,但是你可以 想象一下,这种方法很快变得很尴尬。还有,它 容易出错,与前一个一样。

这是另一种方法,它更不容易出错,而且在概念上非常简单 接近Haskell IO的实际行为:

data ContIO = Exit | PutStrLn String ContIO | GetLine (String -> ContIO) | ...

main :: ContIO
main =
    PutStrLn "what's your name?" $
    GetLine $ \name ->
    PutStrLn ("hi " ++ name ++ "!") $
    Exit

关键是,而不是采取"懒惰列表"作为一个大的回应 在他开始的主要论点,我们提出接受一个的个人请求 一次争论。

我们的程序现在只是一个常规数据类型 - 很像链接列表,除了 你不能正常地遍历它:当RTS解释main时,有时候 它遇到一个像GetLine这样的值来保存一个函数;然后它必须得到 来自stdin的字符串,使用RTS魔法,并将该字符串传递给函数, 在它继续之前。练习:写interpret :: ContIO -> IO ()

请注意,这些实现都不涉及"世界传递"。 "世界通过"并不是I / O在Haskell中的工作原理。实际上 GHC中IO类型的实现涉及一种称为内部类型 RealWorld,但这只是一个实施细节。

Actual Haskell IO添加了一个类型参数,以便我们可以编写动作 "产生"任意值 - 所以它看起来更像data IO a = Done a | PutStr String (IO a) | GetLine (String -> IO a) | ...。这给了我们更多 灵活性,因为我们可以创建" IO行动"产生任意的 值。

(正如Russell O' Connor points out, 这种类型只是一个免费的monad。我们可以轻松地为它编写一个Monad实例。)


那么monad进入哪里呢?事实证明,我们并不需要Monad I / O,我们不需要Monad状态,那么为什么我们需要它呢?该 答案是我们没有。类型类Monad没有什么神奇之处。

但是,当我们使用IOState(以及列表和功能以及 Maybe和解析器和延续传递样式和......)足够长,我们 最终发现他们在某些方面表现得非常相似。我们可能 编写一个打印列表中每个字符串的函数,以及一个运行的函数 列表中的每个有状态计算,并通过状态线程,并且它们都是 看起来非常相似。

由于我们不喜欢编写许多类似的代码,因此我们想要一种方法 摘要; Monad原来是一个伟大的抽象,因为它让我们 抽象的许多类型看起来非常不同,但仍然提供了很多有用的 功能(包括Control.Monad中的所有内容。)

鉴于bindIO :: IO a -> (a -> IO b) -> IO breturnIO :: a -> IO a,我们 可以在Haskell中编写任何IO程序,而无需考虑monad。但 我们最终可能会复制Control.Monad中的很多功能, 例如mapMforever以及when(>=>)

通过实施通用Monad API,我们可以使用完全相同的代码 与解析器和列表一样使用IO操作。这真的是唯一的 我们有Monad类的原因 - 来捕捉它们之间的相似之处 不同类型。

答案 1 :(得分:19)

另一个主要方法是uniqueness typing,与Clean一样。简短的故事是状态句柄(包括现实世界)只能使用一次,而访问可变状态的函数会返回一个新句柄。这意味着第一个调用的输出是秒的输入,强制进行顺序评估。

效果类型在Haskell的Disciple Compiler中使用,但据我所知,在GHC中启用它需要大量的编译器工作。我将把细节的讨论留给那些比我更了解情况的人。

答案 2 :(得分:9)

嗯,首先是什么状态?它可以表现为一个可变变量,你在Haskell中没有。您只有内存引用(IORef,MVar,Ptr等)和IO / ST操作来对其进行操作。

然而,国家本身也可以是纯粹的。要确认审核了' Stream'类型:

data Stream a = Stream a (Stream a)

这是一个价值流​​。但是,解释此类型的另一种方法是更改​​值:

stepStream :: Stream a -> (a, Stream a)
stepStream (Stream x xs) = (x, xs)

当您允许两个流进行通信时,这会很有趣。然后,您将获得自动机类别Auto:

newtype Auto a b = Auto (a -> (b, Auto a b))

这与Stream非常相似,只是现在流在每个瞬间获得类型为 a 的输入值。这形成了一个类别,因此流的一个瞬间可以从另一个流的同一时刻获得其值。

对此的另一种解释是:您有两个随时间变化的计算,并允许它们进行通信。所以每个计算都有本地状态。这是一个与Auto

同构的类型
data LS a b =
    forall s.
    LS s ((a, s) -> (b, s))

答案 3 :(得分:7)

看看A History of Haskell: Being Lazy With Class。它介绍了在发明monad之前在Haskell中执行I / O的两种不同方法:continuation和streams。

答案 4 :(得分:4)

有一种称为功能反应编程的方法,它将时变值和/或事件流表示为第一类抽象。我想到的一个最近的例子是Elm(它是用Haskell编写的,其语法类似于Haskell)。

答案 5 :(得分:1)

我很好奇-可以用纯功能语言(无论是理论上还是现实上)处理I / O或状态的其他方式?

我将添加到此处已经提到的内容中(请注意:这些方法中有些似乎没有,所以有一些“即兴名称” )。

带有免费描述或实现的方法:

其他方法-仅供参考:

  • 系统令牌

    L。奥古斯特森。使用系统令牌的功能性I / O。查尔默斯理工大学计算机科学系PMG Memo 72,S-412 96,哥德堡,1989年。

  • “效果树”

    Rebelsky S.A.(1992)I / O树和交互式惰性函数编程。在:Bruynooghe M.,Wirsing M.(eds)编程语言实现和逻辑编程中。 PLILP1992。计算机科学讲义,第631卷。施普林格,柏林,海德堡。

答案 6 :(得分:0)

它不可能(不是通过"状态"你的意思是" I / O或可变变量行为,如过程语言")。首先,您必须了解monad用于可变变量或I / O的位置。尽管普遍认为,monadic I / O并不是来自像Haskell这样的语言,而是来自像ML这样的语言。 Eugenio Moggi在研究类别理论用于 impure 函数语言(如ML)的指称语义时开发了original monads。要了解原因,请考虑monad(在Haskell中)可以通过三个属性进行分类:

  • (在Haskell中,类型为a)和表达式(在Haskell中,类型为IO a)之间存在区别。
  • 任何值都可以转换为表达式(在Haskell中,将x转换为return x)。
  • 任何值上的函数(返回表达式)都可以应用于表达式(在Haskell中,通过计算f =<< a)。

这些属性显然是(至少)任何不纯函数语言的指称语义:

  • 表达式(如print "Hello, world!\n")可能会产生副作用,但(例如())则不能。所以我们需要在指称语义中区分这两种情况。
  • 可以在需要表达式的任何位置使用值3。因此,我们的指称语义需要一个函数来将值转换为表达式。
  • 一个函数将值作为参数(严格语言中函数的形式参数不具有副作用),但可以应用于表达式。因此,我们需要一种方法将值的表达式返回函数应用于表达式。

所以任何不纯函数(或程序)语言的指称语义都将具有monad结构,即使该结构未明确用于描述如何I / O使用该语言。

纯功能语言怎么样?

在纯函数式语言中有四种主要的I / O方式,我知道(在实践中)(同样,我们将自己局限于程序式I / O; FRP真的是一种不同的范例):

  • Monadic I / O
  • 延续
  • 唯一性/线性类型
  • 对话框

Monadic I / O很明显。基于延续的I / O看起来像这样:

main k = print "What is your name? " $
    getLine $ \ myName ->
    print ("Hello, " ++ myName ++ "\n") $
    k ()

每个I / O操作都会进行“继续”,执行其操作,然后尾部调用(在引擎盖下)继续。所以在上面的程序中:

  • print "What is your name? "运行,然后
  • getLine运行,然后
  • print ("Hello, " ++ myName ++ "\n")运行,然后
  • k运行(将控制权返回给操作系统)。

延续monad是对上述的一个明显的语法改进。更重要的是,语义,我只能看到两种方法使 I / O 实际上在上面工作:

  • 使I / O操作(和延续)返回&#34; I / O类型&#34;描述您要执行的I / O.现在你有一个没有newtype包装器的I / O monad(基于monad的延续)。
  • 使I / O操作(和延续)返回基本上()的内容,并将I / O作为调用各个操作的副作用(例如print,{{1等等)。但是,如果用您的语言(上面getLine定义的右侧)对表达式进行评估是有效的,我不会认为这纯粹是功能性的。

唯一性/线性类型怎么样?这些使用特殊的令牌&#39;值表示每个动作后的世界状态,并强制执行排序。代码如下所示:

main

线性类型和唯一性类型之间的区别在于线性类型,结果必须是main w0 = let w1 = print "What is your name? " w0 (w2, myName) = getLine w1 w3 = print $ "Hello, " ++ myName ++ "!\n" in w3 (它必须是w3类型),而在唯一性类型,结果可能类似于World。只需评估w3 `seq` ()是否会发生I / O.

同样,状态monad是对上述的明显的语法改进。更重要的是,语义,你又有两个选择:

  • w3参数中进行I / O操作,例如printgetLine严格(因此先前的操作首先运行,并且副作用(所以I / O恰好是评估它们的副作用。)同样,如果你有评价的副作用,我认为这不是真正的功能。
  • 使World类型实际代表需要执行的I / O.这与使用尾递归程序的GHC World实现具有相同的问题。假设我们将IO的结果更改为main。现在main w3尾巴调用自己。任何以纯函数式语言尾部调用自身的函数都没有任何价值(只是一个无限循环);这是关于递归的指称语义如何在纯语言中工作的基本事实。同样,我不会认为违反该规则的任何语言(特别是像main这样的特殊数据类型)纯粹是功能性的。

所以,真的,唯一性或线性类型a)如果你将它们包装在状态monad中,那么产生更清晰/更清晰的程序; b)实际上是一种在纯功能语言中进行I / O操作的方法所有

对话框怎么样?这是做I / O的唯一方法(或者,技术上,可变的变量,尽管这很难),它们既纯粹是功能性的,又独立于monad。看起来像这样:

World

但是,您会注意到这种方法的一些缺点:

  • 目前尚不清楚如何将I / O执行程序纳入此方法。
  • 您必须使用数字或位置索引来查找与给定请求相对应的响应,这非常脆弱。
  • 在收到回复之后,没有明显的方法来确定回应的范围;如果此程序在发出相应的main resps = [ PrintReq "What is your name? ", GetLineReq, PrintReq $ "Hello, " ++ myName ++ "!\n" ] where LineResp myName = resps !! 1 请求之前以某种方式使用myName,则编译器将接受您的程序,但它会在运行时死锁。

解决所有这些问题的一种简单方法是将对话框包含在延续中,如下所示:

getLine

现在,代码看起来与先前给出的继续传递接近I / O的代码相同。实际上,对话框是基于延续的I / O系统中延续的优秀结果类型,甚至是基于monad的连续monadic I / O系统。但是,通过转换回continuation,同样的参数适用,所以我们看到,即使运行时系统在内部使用对话框,程序仍应编写为以monadic风格进行I / O操作

相关问题