为什么我不能这样做:
import Data.Char
getBool = do
c <- getChar
if c == 't'
then IO True
else IO False
而不是使用return
?
为什么我不能这样做:
import Data.Char
getBool = do
c <- getChar
if c == 't'
then IO True
else IO False
而不是使用return
?
我将回答稍微更广泛(也更有趣)的问题。这是因为至少从语义的角度来看,不止一个 IO 构造函数。有不止一种“种类”的IO
价值。我们可以认为可能有一种IO
值用于打印到屏幕上,一种IO
值用于从文件中读取,等等。
我们可以想象,为了推理,IO被定义为
data IO a = ReadFile a
| WriteFile a
| Network a
| StdOut a
| StdIn a
...
| GenericIO a
每一种行为都有一种价值IO
。(但是,请记住,这实际上并不是如何IO
实现的。IO
除非您是编译器黑客,否则最好不要玩弄魔法。)
现在,有趣的问题是——为什么他们做到了这样我们就不能手动构建这些?为什么他们没有导出这些构造函数,以便我们可以使用它们?这就引出了一个更广泛的问题。
这基本上有两个原因——第一个可能是最明显的一个。
如果您可以访问构造函数,则还可以访问可以在其上进行模式匹配的de- constructor。考虑Maybe a
类型。如果我给你一个Maybe
值,你可以Maybe
通过模式匹配提取“内部”的任何内容!这很简单。
getJust :: Maybe a -> a
getJust m = case m of
Just x -> x
Nothing -> error "blowing up!"
想象一下,如果您可以使用IO
. 那将意味着IO
将不再安全。你可以在纯函数中做同样的事情。
getIO :: IO a -> a
getIO io = case io of
ReadFile s -> s
_ -> error "not from a file, blowing up!"
这很糟糕。如果您有权访问IO
构造函数,则可以创建一个将IO
值转换为纯值的函数。太糟糕了。
所以这是不导出数据类型的构造函数的一个很好的理由。如果你想保持一些数据“秘密”,你必须对你的构造函数保密,否则有人可以通过模式匹配提取他们想要的任何数据。
这个原因对于面向对象的程序员来说是很熟悉的。当您第一次学习面向对象编程时,您会了解到对象有一个特殊的方法,当您创建一个新对象时会调用该方法。在这种方法中,您还可以初始化对象内字段的值,最好的事情是 - 您可以对这些值执行完整性检查。您可以确保这些值“有意义”,如果没有,则抛出异常。
好吧,你可以在 Haskell 中做同样的事情。假设您是一家拥有几台打印机的公司,并且您想跟踪它们的使用年限以及它们位于建筑物的哪一层。所以你写了一个 Haskell 程序。您的打印机可以这样存储:
data Printer = Printer { name :: String
, age :: Int
, floor :: Int
}
现在,您的建筑物只有 4 层,并且您不想意外地说您在 14 层有一台打印机。这可以通过不导出Printer
构造函数来完成,而是使用一个mkPrinter
为您创建打印机的函数,如果所有参数有意义。
mkPrinter :: String -> Int -> Maybe Printer
mkPrinter name floor =
if floor >= 1 && floor <= 4
then Just (Printer name 0 floor)
else Nothing
如果您mkPrinter
改为导出此功能,则您知道没有人可以在不存在的楼层上创建打印机。
您可以使用IO
而不是return
. 但没那么容易。而且你还需要导入一些内部模块。
让我们看看来源Control.Monad
:
instance Monad IO where
{-# INLINE return #-}
{-# INLINE (>>) #-}
{-# INLINE (>>=) #-}
m >> k = m >>= \ _ -> k
return = returnIO
(>>=) = bindIO
fail s = failIO s
returnIO :: a -> IO a
returnIO x = IO $ \ s -> (# s, x #)
但即使要使用 IO
而不是return
,您也需要导入GHC.Types(IO(..))
:
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
在此之后,您可以编写IO $ \ s -> (# s, True #)
( IO
is a State) 而不是return True
:
解决方案:
{-# LANGUAGE UnboxedTuples #-} -- for unboxed tuples (# a, b #)
{-# LANGUAGE TupleSections #-} -- then (,b) == \a -> (a, b)
import GHC.Types (IO (..))
import Data.Char
getBool = do
c <- getChar
if c == 't'
then IO (# , True #)
else IO (# , False #)
IO
单子和单子几乎没有魔法ST
,比大多数人认为的要少得多。
可怕的 IO 类型只是在GHC.Primnewtype
中定义的:
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
首先,从上面可以看出,IO
constructor 的参数和 的参数是不一样的return
。通过查看State
monad 的简单实现,您可以得到更好的主意:
newtype State s a = State (s -> (s, a))
其次,IO 是一种抽象类型:不导出构造函数是有意决定的,因此您既不能构造IO
也不能模式匹配它。这允许 Haskell 强制引用透明性和其他有用的属性,即使存在输入输出。