1

我们知道,在 Haskell 程序中,几乎每一次计算都会返回一些东西,而这样的返回值可以被另一个计算捕获,以对其进行更多的转换。所以如果我们“扁平化”一个普通的 Haskell 程序,它应该是:

-- pure `a`: comes from Hask; not from file, network or any 
-- other side-effected devices

a → a' → a'' → a''' → .... → a_fin

当然,这个纯值可能是“上下文”的。但我们仍然可以追溯交替的路径:

a → m a → m a' → a'' → n a''' → ... → z a_fin

对我来说,这表明我们可以控制我们的程序以避免副作用和其他“意外”,这可能是由于缺少类型系统或我们自己造成的。但是当IO ()出现时,似乎缺少:

--!! What happened between the IO () and the next computation ?

a → m a → m a' → IO () >> b → b' → m b'  

似乎什么都没有传递/接收,IO ()但它至少必须读/写一些东西。特别是如果我们考虑“接收者”过程:

Sender::   a → m a → m a' → IO () >> b → b' → m b' ... → m b_fin
Receiver:: IO a' → IO a'' → IO a''' → ... → IO a_fin

a在发件人中,我们看不到IO (). 但如果我们同时考虑这两个过程,缺失的部分又回来了!所以我们可以说我们错过了或没有错过信息,根据你的观点。这是信息泄漏,我们只是放弃了对程序的控制,当我们将 放入IO ()程序中时?

谢谢 !

PS。哦,我还发现接收器只能从“上下文”值开始计算,而不是纯值,这是我脑海中出现的另一个问题......

4

6 回答 6

8

从您的评论看来,您认为因为IO ()-typed 计算没有返回有用的东西,所以类型系统不能保证您的程序是正确的。

首先,类型系统保证程序的正确性,除非是简单的情况。在复杂的程序中,完全有可能犯逻辑错误,你的程序会编译但返回错误的结果。避免逻辑错误是程序员的职责,而类型系统只是一种(确实是强大的)工具。

第二点是从第一点开始的。IO是一个普通的单子;它与任何其他类型都是相同的类型(当然,从类型系统的角度来看)。AFAIK 它没有从类型系统中得到一些特殊处理。type 的值IO ()确实意味着“不纯的计算,在执行时可能会以某种方式影响外部世界,并且不会产生任何有意义的东西”,仅此而已。考虑 type 的值State Int ():它的意思是“一个有状态的计算,当执行时,它可能会对当前的 type 状态做一些事情,Int并且不会产生任何有用的东西”。你看,这两个值都有某种副作用,并且它们都具有与计算结果相关的相同含义. 从类型系统的角度来看,它们是等价的。但第二个是完全纯粹的计算。您可以使用或轻松将其转换为有意义的值(在本例中为Int)。execStaterunState

于 2012-06-29T17:53:21.887 回答
3

不。您认为链中的每个动作只能看到其前一个动作的结果;但实际上,如果需要,每个操作都可以访问任何先前操作的结果。只是使用一个玩具示例:

return 5 >>= (\x -> putStrLn "mwahaha!" >>= (\_ -> putStrLn "x is " ++ show x >>= (\_ -> return ())))

注意变量的范围x——它延伸到整个表达式的末尾。(括号在那里是可选的,但我把它们放在里面是为了使范围更明显。)

再次考虑 的类型>>=

(>>=) :: Monad m => m a -> (a -> m b) -> m b

这可以解释为“使用类型动作的结果和类型m a函数a -> m b来构造程序的其余部分”(不仅仅是下一个动作)。

动作还具有可变内存和程序可用的任何 I/O 设备的上下文,因此这也是动作可以与另一个进行通信的另一种机制。就你所知,两种类型的动作IO ()可以通过共享内存或共享文件进行通信。

于 2012-06-29T16:02:55.880 回答
1

是的,从某种角度来看,类型的值IO a是凭空提取信息的;并非源自生成它们的函数的输入的信息。这是不可避免的,因为它的全部意义在于IO编写其结果取决于(并且可以影响)程序外部世界的计算。调用产生的信息readFile驻留在磁盘上的文件中,而不是在您的程序中。所以是的,你“放弃了控制”,因为任何程序使用任何IO动作的结果都取决于不受你正在编写的程序控制的东西。但是每个程序都是这样的;避免它的唯一方法是不使用IO(或任何与外界通信的机制),然后你的程序只是写下最终结果的一种非常复杂的方式。

但是类型系统从不关心实际的输入/输出,并且它不能证明它们是正确的,即使IO不涉及。在类型检查中,Integer -> String它所做的只是验证它是否使用确实接受 aInteger并返回 a 的操作来实现String。它知道是否生成了正确的字符串。

你甚至可以对类型系统“撒谎”;undefined任何函数的有效定义,然后它为您提供一个函数,例如,就类型系统而言,它接受一个整数并返回一个字符串。但这并不意味着它是该功能的正确实现;并且根据这个定义的任何其他函数也是不正确的,类型检查器根本不在乎。

同样,类型函数Integer -> IO ()检查该函数是否使用接受Integer和产生的操作IO ()。这就是类型检查所证明的全部IO ()内容,这与 forIO Integer或 forInteger任何其他类型没有什么不同。

于 2012-07-01T23:55:03.520 回答
0

该链由程序的“状态”组成(嗯,有点)。让我们考虑一个简单的程序:

main = do
  let a = 4  -- 1
  print a    -- 2
  print a    -- 3

在这里,在第 1 步之后,您的状态为4。但是在第 2 步之后,这4并没有消失——你现在的状态是 (4, ())。在第 3 步之后是 (4, (), ()) 等等。


长话短说,稍后将使用的信息保留在链中。之后不会消失IO ()

于 2012-06-29T10:46:40.873 回答
0

我不太确定问题是什么,但无论如何,如果我的直觉是正确的,我会给出答案。

事实是,如果你只看程序的组成函数的类型,你可以得出以下两个结论之一:

  1. 您的程序不使用 IO,因此可以保证计算是纯的,因此不会产生副作用(当然,在一定范围内。)

或者

  1. 您的程序确实使用了 IO,因此不是纯粹的,并且不能保证它不会产生副作用。

当然,在所有可能的 IO 计算范围内都有一些计算不会产生副作用,就像有些计算的类型表明纯度一样,但使用了不安全的函数,例如unsafePerformIO. 但总的来说,您可以查看纯函数的类型并说:此函数不会在磁盘 IO、网络 IO 或任何类型的情况下失败。同样的保证不适用于 IO 计算。

于 2012-06-29T13:57:57.403 回答
0

IO ()意味着计算可能会受到过去IO行为的影响,也可能会影响“计算链”中的未来IO行为。但是,后续的非 IO 值(如果它有一个 monad 堆栈,则在堆栈中的任何位置都没有 IO)不能直接受到影响,因为()这是微不足道的值,这就是它们被允许依赖的全部。如果它们依赖于一些受影响的东西,它们可能会受到间接IO a影响。这是 ignoring unsafePerformIO,只能以上述解释仍然基本成立的方式使用。

于 2012-06-29T19:01:58.347 回答