Haskell - “IO()”是否意味着我们放弃对程序的控制?

时间:2012-06-29 10:28:53

标签: haskell functional-programming

我们知道在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。哦,我还发现接收器,只能用“上下文”值开始计算,而不是纯值,这是另一个问题出现在我脑海中......

6 个答案:

答案 0 :(得分:7)

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

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

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

答案 1 :(得分: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 ()类型的两个操作可以通过共享内存或共享文件进行通信。

答案 2 :(得分:1)

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

但类型系统从不关心实际输入/输出,并且即使不涉及IO也不能证明它们是正确的。在类型检查Integer -> String类型的函数时,它所做的就是验证它是否实际上接受Integer并返回String的操作。 无概念是否生成了正确的字符串。

你甚至可以“骗”到类型系统; undefined任何函数的有效定义,然后为您提供一个函数,例如获取一个Integer并返回一个String,就类型系统而言。但这并不意味着它是该功能的正确实现;并且根据这个定义的任何其他功能也是不正确的,类型检查器根本不关心。

同样,类型Integer -> IO ()的函数会检查函数是否使用接受Integer的操作并生成IO ()。这种类型检查的全部证明了这一点,IO ()IO IntegerInteger任何其他}没有什么不同类型。

答案 3 :(得分:0)

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

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

此处,在第1步之后,您的州为4。但是在第2步之后,这个4并没有消失 - 你的状态现在是(4,())。在第3步之后,它是(4,(),()),依此类推。


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

答案 4 :(得分:0)

我不太清楚问题是什么,但无论如何我会直截了当地回答这个问题。

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

  1. 您的程序不使用IO,因此保证计算是纯粹的,因此不会产生任何副作用(当然,在某些限制内)。
    1. 您的程序确实使用IO,因此不是纯粹的,并且不能保证它不会产生副作用。
    2. 当然,在所有可能的IO计算的范围内都有计算产生没有副作用的计算,就像有类型表明纯度的计算一样,但是使用不安全的函数,例如unsafePerformIO。但一般来说,您可以查看纯函数的类型并说:此函数不会在磁盘IO,网络IO或任何类型的任何事件上失败。同样的保证不适用于IO计算。

答案 5 :(得分:0)

IO ()表示计算可能会受到过去IO行为的影响,并且可能会影响“计算链”中的未来IO行为。但是,之后的非IO值(如果它具有monad堆栈,在堆栈中的任何位置没有IO的任何内容)都不会直接受到影响,因为()是微不足道的值,这就是他们可以依赖的全部。如果它们依赖于受影响的某些IO a,它们可能会间接受到影响。这忽略了unsafePerformIO,只能以上述解释仍然基本成立的方式使用。{/ p>