为什么我不能这样做:
import Data.Char
getBool = do
c <- getChar
if c == 't'
then IO True
else IO False
而不是使用return
?
答案 0 :(得分:33)
我会回答稍微宽泛(也更有趣)的问题。这是因为至少从语义角度来看,存在多个IO构造函数。 IO
值不止一种“种类”。我们可以认为在屏幕上打印可能有一种IO
值,从文件中读取的值为IO
,等等。
我们可以想象,为了推理,IO被定义为类似
data IO a = ReadFile a
| WriteFile a
| Network a
| StdOut a
| StdIn a
...
| GenericIO a
对于每种IO
行为都有一种价值。 (但是,请记住,实际上并不是IO
的实现方式。IO
是最好的魔法,除非你是编译器黑客,否则不要玩弄它。)
现在,有趣的问题 - 他们为什么要这样做,以便我们不能手动构建这些?为什么他们没有导出这些构造函数,以便我们可以使用它们?这导致了一个更广泛的问题。
这基本上有两个原因 - 第一个可能是最明显的原因。
如果您有权访问构造函数,那么您还可以访问 de - 构造函数,您可以在其上进行模式匹配。想想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
构造函数来完成,而是使用函数{ {1}}如果所有参数都有意义,它会为您创建打印机。
mkPrinter
如果您导出此mkPrinter :: String -> Int -> Maybe Printer
mkPrinter name floor =
if floor >= 1 && floor <= 4
then Just (Printer name 0 floor)
else Nothing
函数,则您知道没有人可以在不存在的楼层上创建打印机。
答案 1 :(得分:13)
您可以使用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
是州)而不是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 #)
答案 2 :(得分:3)
IO
和ST
monad周围的魔法很少,比大多数人认为的要少得多。
可怕的IO类型只是GHC.Prim中定义的newtype
:
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
首先,如上所述,IO
构造函数的参数与return
的参数不同。通过查看State
monad:
newtype State s a = State (s -> (s, a))
其次,IO是一种抽象类型:它是故意决定不导出构造函数,因此您既不能构造IO
也不能模式匹配它。这允许Haskell即使存在输入输出也可以强制引用透明度和其他有用的属性。