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

アレッ*_*ックス 15 haskell

为什么我不能这样做:

import Data.Char

getBool = do
  c <- getChar
  if c == 't' 
    then IO True 
    else IO False
Run Code Online (Sandbox Code Playgroud)

而不是使用return

kqr*_*kqr 35

背景

我会回答稍微宽泛(也更有趣)的问题.这是因为至少从语义角度来看,存在多个IO构造函数.有一种以上的"种类"的IO价值.我们可以认为IO在屏幕上打印可能有一种IO值,从文件读取有一种值,等等.

我们可以想象,为了推理,IO被定义为类似的东西

data IO a = ReadFile a
          | WriteFile a
          | Network a
          | StdOut a
          | StdIn a
          ...
          | GenericIO a
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

建筑师也是解构主义者

如果你有机会获得一个构造函数,你也有机会获得一 -constructor,你可以做模式匹配上.想想Maybe a类型.如果我给你一个Maybe值,你可以Maybe通过模式匹配提取"内部"的内容!这很简单.

getJust :: Maybe a -> a
getJust m = case m of
              Just x -> x
              Nothing -> error "blowing up!"
Run Code Online (Sandbox Code Playgroud)

想象一下,如果你能做到这一点IO.这意味着IO将不再安全.你可以在纯函数中做同样的事情.

getIO :: IO a -> a
getIO io = case io of
             ReadFile s -> s
             _ -> error "not from a file, blowing up!"
Run Code Online (Sandbox Code Playgroud)

这很糟糕.如果您可以访问IO构造函数,则可以创建一个将IO值转换为纯值的函数.太糟糕了.

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

你不想允许任何价值

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

好吧,你可以在Haskell中做同样的事情.假设您是一家拥有少量打印机的公司,并且您想要跟踪它们的年龄以及它们所在建筑物的楼层.所以你写了一个Haskell程序.您的打印机可以像这样存储:

data Printer = Printer { name :: String
                       , age :: Int
                       , floor :: Int
                       }
Run Code Online (Sandbox Code Playgroud)

现在,你的建筑只有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
Run Code Online (Sandbox Code Playgroud)

如果您导出此mkPrinter功能,则您知道没有人可以在不存在的楼层上创建打印机.

  • @KevinMeredith当存在副作用时,许多正常的Haskell编程技术会迅速破解.如果我们有一个函数`IO a - > a`,我们可以在看起来很纯净的函数中"隐藏"副作用,然后很多东西会破坏. (2认同)

vio*_*ior 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 #)
Run Code Online (Sandbox Code Playgroud)

但即使使用 IO而不是return,您需要导入GHC.Types(IO(..)):

newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
Run Code Online (Sandbox Code Playgroud)

在此之后,你可以写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 #)
Run Code Online (Sandbox Code Playgroud)

  • @nponeccop.这一次,没有."`RealWorld`非常神奇......它只用在类型系统中,用于参数化`State #`"."'国家'#是原始的,未提升型状态....类型参数的唯一目的是让不同状态的线程中分离出来.它是由什么都没有表示." (2认同)

npo*_*cop 5

IO周围和单子几乎没有什么魔力ST,比大多数人想象的要少得多。

可怕的 IO 类型只是newtypeGHC.Prim中定义的:

newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
Run Code Online (Sandbox Code Playgroud)

首先,如上所示,IO构造函数的参数与 的参数不同return。通过查看Statemonad 的简单实现,您可以得到更好的想法:

newtype State s a = State (s -> (s, a))
Run Code Online (Sandbox Code Playgroud)

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