为什么我不能使用IO构造函数

时间:2013-09-30 11:24:51

标签: haskell

为什么我不能这样做:

import Data.Char

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

而不是使用return

3 个答案:

答案 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是最好的魔法,除非你是编译器黑客,否则不要玩弄它。)

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

为什么不想为数据类型导出构造函数?

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

1。构造函数也是解构器

如果您有权访问构造函数,那么您还可以访问 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值转换为纯值的函数。太糟糕了。

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

2。您不希望允许任何值

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

好吧,你可以在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)

IOST 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即使存在输入输出也可以强制引用透明度和其他有用的属性。