IO Monads:GHC实现意义与数学意义

时间:2017-01-29 22:55:40

标签: haskell monads

考虑

ioFunction :: String -> IO ()
ioFunction str = do putStrLn str
                    putStrLn "2"

在这里,从数学的角度来看,ioFunction是一个函数,它接受一个输入并返回类型IO ()的值。我认为这意味着什么。也就是说,从数学的角度来看,这个函数返回一个值,而不是别的;特别是,它不会打印任何东西。

这是否意味着Haskell使用IO monad进行命令性副作用的方式(在这种情况下:运行此函数将首先打印str然后按顺序打印“2”)< strong>纯粹的GHC实现细节与术语的数学(在某种意义上甚至是Haskell)无关?

编辑:为了使这个问题更清楚,我的意思是,例如,以下两个函数之间的数学观点有什么不同:

ioFunction1 :: String -> IO ()
ioFunction1 str = do putStrLn str
                     putStrLn "2"


ioFunction2 :: String -> IO ()
ioFunction2 str = do return ()

似乎答案是否定的,因为 - 从数学的角度来看 - 它们都将String作为输入并返回IO ()类型的值(可能是相同的值) 。情况不是这样吗?

3 个答案:

答案 0 :(得分:5)

  

在这里,从数学的角度来看,ioFunction是一个函数,它接受一个输入并返回类型IO ()的值。我认为这意味着什么。

是。准确。

  

这是否意味着Haskell使用IO monad进行命令性副作用的方式(在这种情况下:运行此函数将首先打印str然后print "2",按此顺序)是纯粹是一个GHC实现细节,与术语的数学(在某种意义上甚至是Haskell)无关?

不完全。从数学(集合论)的角度来看,我认为“相同”意味着结构相同。由于我不知道IO ()的值是什么,我无法说明该类型的两个值是否相同。

事实上,这是设计上的:通过使IO不透明(在我不知道什么构成IO的意义上),GHC阻止我永远不能说那个类型IO ()的值等于另一个。我可以通过IOfmap(<*>)(>>=)等功能公开mplus所能解决的问题。

答案 1 :(得分:4)

我总是觉得考虑IO monad的简化“命令式语言源代码”实现很有帮助:

data IO :: * -> * where
   PutStr :: String -> IO ()
   GetLine :: IO String
   IOPure :: a -> IO a
   IOSeq :: IO a -> IO b -> IO b
   ...
   LaunchMissiles :: IO ()

那么ioFunction在数学意义上显然是一个恰当的,合理的函数:

ioFunction str = do putStrLn str
                    putStrLn "2"
               = putStr (str++"\n") >> putStrLn "2\n"
               = IOSeq (PutStr (str++"\n")) (PutStrLn "2\n")

这只是一个有效表示命令式语言的一些源代码的数据结构。 ioFunction将给定的参数放在结果结构中的特定位置,因此它在数学上不仅仅是一个简单的“return ()而且没有别的(可能会发生某些事情,但它是一个GHC实现细节) )”

ioFunction2的值确实完全不同:

ioFunction2 str = do return ()
                = return ()
                = IOPure ()
  

我们怎么知道他们在这个意义上是不同的?

这是一个很好的问题。实际上,你通过执行两个程序并观察它们会产生不同的效果来了解它,因此它们must have been different。这当然有点尴尬 - “观察发生的事情”不是数学,而是物理学,观察将科学地要求你在完全相同的环境条件下执行两次。

更糟糕的是,即使纯数值通常在数学上是相同的,您也可以观察到不同的行为:print 100000000会立即引起副作用,而print $ last [0..100000000]则会产生明显的停顿(其中,如果你用时间打印命令跟着它,实际上可以产生不同的文本输出。)

但这些问题在现实世界中是不可避免的。因此,只有Haskell没有在IO上指定任何可以用数学严谨性来检查语言中的相等性的结构。所以,在某种意义上,你真的不能知道putStrLn str >> putStrLn "2"return ()不一样。

实际上他们可能是一样的。可以想象的是,IO比以上更简单的玩具实现:

data IO :: * -> * where
   IONop :: IO ()
   IOHang :: IO a -> IO a

并简单地将任何纯输出操作映射到no-op(以及任何无限循环的输入)。那么你会有

ioFunction str = do putStrLn str
                    putStrLn "2"
               = IONop >> IONop
               = IONop

ioFunction2 str = return ()
                = IONop

这有点像我们在自然数上强加了一个额外的公理,即1 = 0.然后你可以得出结论每个数字为零。

显然,这将是一个完全无用的实现。但是,如果你在沙盒环境中运行Haskell代码并且想要完全确定没有任何不好的事情发生,那么类似的东西实际上是有意义的。 http://tryhaskell.org/做了类似的事情。

答案 2 :(得分:0)

为简单起见,让我们专注于IO monad的输出方面。从数学上讲,输出(作者)monad由endofunctor $ T $给出,$ T(A)= U ^ * \ times A $,其中$ U $是一组固定的字符,$ U ^ * $是超过$ U $的字符串集。然后$ ioFunction:U ^ * \到T()$,即$ ioFunction:U ^ * \到U ^ * \ times()$,和$ ioFunction(s)=(s ++“\ n”++“2 \ N”,())$。相比之下,$ ioFunction2(s)=(“”,())$。任何实现都必须尊重这种差异。差异主要是数学上的,而不是实现细节。 (嗯,TeX标记似乎只适用于mathoverflow ...)