16

为什么我不能这样做:

import Data.Char

getBool = do
  c <- getChar
  if c == 't' 
    then IO True 
    else IO False

而不是使用return

4

3 回答 3

35

背景

我将回答稍微更广泛(也更有趣)的问题。这是因为至少从语义的角度来看,不止一个 IO 构造函数。有不止一种“种类”的IO价值。我们可以认为可能有一种IO值用于打印到屏幕上,一种IO值用于从文件中读取,等等。

我们可以想象,为了推理,IO被定义为

data IO a = ReadFile a
          | WriteFile a
          | Network a
          | StdOut a
          | StdIn a
          ...
          | GenericIO a

每一种行为都有一种价值IO。(但是,请记住,这实际上并不是如何IO实现的。IO除非您是编译器黑客,否则最好不要玩弄魔法。)

现在,有趣的问题是——为什么他们做到了这样我们就不能手动构建这些?为什么他们没有导出这些构造函数,以便我们可以使用它们?这就引出了一个更广泛的问题。

为什么不希望导出数据类型的构造函数?

这基本上有两个原因——第一个可能是最明显的一个。

1.构造函数也是解构函数

如果您可以访问构造函数,则还可以访问可以在其上进行模式匹配的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值转换为纯值的函数。太糟糕了。

所以这是不导出数据类型的构造函数的一个很好的理由。如果你想保持一些数据“秘密”,你必须对你的构造函数保密,否则有人可以通过模式匹配提取他们想要的任何数据。

2.你不想允许任何值

这个原因对于面向对象的程序员来说是很熟悉的。当您第一次学习面向对象编程时,您会了解到对象有一个特殊的方法,当您创建一个新对象时会调用该方法。在这种方法中,您还可以初始化对象内字段的值,最好的事情是 - 您可以对这些值执行完整性检查。您可以确保这些值“有意义”,如果没有,则抛出异常。

好吧,你可以在 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改为导出此功能,则您知道没有人可以在不存在的楼层上创建打印机。

于 2013-09-30T12:00:36.867 回答
15

您可以使用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 #)( IOis 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 #)
于 2013-09-30T12:47:46.863 回答
4

IO单子和单子几乎没有魔法ST,比大多数人认为的要少得多。

可怕的 IO 类型只是在GHC.Primnewtype中定义的:

newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))

首先,从上面可以看出,IOconstructor 的参数和 的参数是不一样的return。通过查看Statemonad 的简单实现,您可以得到更好的主意:

newtype State s a = State (s -> (s, a))

其次,IO 是一种抽象类型:不导出构造函数是有意决定的,因此您既不能构造IO也不能模式匹配它。这允许 Haskell 强制引用透明性和其他有用的属性,即使存在输入输出。

于 2013-09-30T12:10:24.773 回答