Haskell中的IO类型是什么?

3

我对Haskell编程语言很陌生,经常在函数参数或返回类型中遇到IO类型。

playGame :: Screen -> IO ()

或者

gameRunner :: IO String -> (String -> IO ()) -> Screen -> IO ()

这个是怎么工作的,我有点困惑,因为我知道String类型需要单词和Int类型需要数字。在函数中使用的IO期望或返回什么?


2
也许你应该读一下像 LYAH(http://learnyouahaskell.com/input-and-output)上的 IO 教程。 - chi
3个回答

7

IO是Haskell区分具有引用透明性和没有的代码的方式。 IO a是返回a的IO操作的类型。

你可以把IO操作看作一段带有对真实世界的某些影响的等待执行的代码。由于这种副作用,IO操作不是引用透明的;因此,执行顺序很重要。Haskell程序的main函数的任务是适当地序列化和执行所有的IO操作。因此,当你编写一个返回IO a的函数时,你实际上是编写了一个返回动作的函数,该动作最终 - 在被main执行时 - 执行该动作并返回一个a

更多解释:

引用透明意味着你可以用它的值替换一个函数。引用透明的函数不能有任何副作用; 特别地,引用透明的函数不能访问任何硬件资源,如文件、网络或键盘,因为函数值会依赖于除它的参数之外的其他东西。

在像Haskell这样的函数式语言中,具有引用透明性的函数就像数学函数(定义域和值域之间的映射)一样,而不是用于计算函数值的命令序列。因此,Haskell代码告诉编译器一个函数是“应用”到它的参数上,但它并不表示函数被“调用”并实际计算。

因此,引用透明的函数不意味着执行顺序。Haskell编译器可以自由地以任何方式评估函数 - 或者根本不评估它们(称为惰性求值)。唯一的排序来自于数据依赖关系,当一个函数需要另一个函数的输出作为输入时。

现实世界的副作用不具有引用透明性。你可以把真实世界看作某种隐含的全局状态,影响性函数改变这个状态。由于这个状态,执行顺序很重要:如果你先从数据库读取再更新它,或者反过来,就会产生差异。

Haskell是一种纯函数式语言,它的所有函数都具有引用透明性,编译基于这个保证。那么,我们如何处理操作全局真实世界状态的具有效果的函数,并以特定的顺序执行它们?通过在这些函数之间引入数据依赖关系。

这正是IO所做的:在底层,IO类型将一个具有效果的函数包装在一个虚拟状态参数中。每个IO操作将此虚拟状态作为输入并提供其作为输出。从一个IO操作传递这个虚拟状态参数到下一个IO操作创建一个数据依赖,从而告诉Haskell编译器如何正确地序列化所有IO操作。

你看不到虚拟状态参数,因为它被一些语法糖隐藏起来:在main和其他IO操作中的do符号以及在IO类型内部。


2
简要概括一下:
f1 :: A -> B -> C

这是一个函数,接受两个类型为AB的参数,并返回一个C。它不执行任何IO操作。

f2 :: A -> B -> IO C

f1类似,但还可以执行IO操作。

f3 :: (A -> B) -> IO C

该函数接受一个不执行IO操作的函数A -> B作为参数,并生成一个C,可能执行IO操作。

f4 :: (A -> IO B) -> IO C

接受一个可以执行IO操作的函数 A -> IO B 作为参数,并生成可能执行IO操作的 C

f5 :: A -> IO B -> IO C

接受类型为A的值作为参数,类型为IO B的IO操作,并返回可能执行IO(例如通过运行一个或多个IO操作参数)的C类型的值。

示例:

f6 :: IO Int -> IO Int
f6 action = do
   x1 <- action
   x2 <- action
   putStrLn "hello!"
   x3 <- action
   return (x1+x2+x3)

当一个函数返回IO ()时,它并不返回有用的值,但可以执行IO操作。类似于C或Java中返回void。您的

gameRunner :: IO String -> (String -> IO ()) -> Screen -> IO ()

该函数可使用以下参数进行调用:

arg1 :: IO String
arg1 = do
   putStrLn "hello"
   s <- readLine
   return ("here: " ++ s)

arg2 :: String -> IO ()
arg2 str = do
   putStrLn "hello"
   putStrLn str
   putStrLn "hello again"

arg3 :: Screen
arg3 = ... -- I don't know what's a Screen in your context

我正在尝试运行您的示例函数,但它失败了:f6 :: IO Int -> IO Int f6 action = do x1 <- action x2 <- action putStrLn "hello!" x3 <- action return (x1+x2+x3) - Oto-Obong Eshiett
@Oto-ObongEshiett 你怎么运行它?你传递了什么参数?尝试一下,例如 f6 (do putStrLn "hi" ; return 12) - chi

0

让我们先尝试回答一些更简单的问题:

  • Haskell 中的 Maybe 类型是什么?

    来自Haskell 2010 Report第21章(第205页):

    data Maybe a = Nothing | Just a
    

    它是一个简单的部分类型 - 你有一个值(通过 Just 传递)或者没有(Nothing)。

  • 这是如何工作的?

    让我们看一下 Maybe 的一个可能的 Monad 实例:

    instance Monad Maybe where
        return = Just
        Just x  >>= k = k x
        Nothing >>= _ = Nothing
    

    这个单子界面简化了基于 Maybe 构造函数的值的使用,例如:

    \f ox oy -> case ox of
                  Nothing -> Nothing
                  Just x  -> case oy of
                               Nothing -> Nothing
                               Just y  -> Just (f x y)
    

    你可以简单地写成:

    \f ox oy -> ox >>= \x -> oy >>= \y -> return (f x y)
    

    单子界面是广泛适用的:从解析到封装状态,等等。

  • 函数中使用的 Maybe 类型期望或返回什么?

    对于一个期望基于 Maybe 的值的函数,例如:

    maybe :: b -> (a -> b) -> Maybe a -> b
    maybe _ f (Just x) = f x
    maybe d _ Nothing  = d
    

    如果在函数中使用了它的内容,则函数可能需要处理无法使用的值,即 Nothing

    对于返回基于 Maybe 的值的函数,例如:

    invert :: Double -> Maybe Double
    invert 0.0 = Nothing
    invert d   = Just (1/d)
    

    它只需要使用适当的构造函数。

    最后一点:观察如何使用基于 Maybe 的值 - 从简单开始(例如 invert 0.5Just "here"),然后定义其他可能更复杂的基于 Maybe 的值(使用 (>>=)(>>) 等),最终通过模式匹配直接检查,或通过适当的定义(maybefromJust 等)进行抽象。


原始问题的时间:

  • Haskell中的IO类型是什么?

    来自报告第6.1.7节(第75页):

    IO类型用作与外部世界交互的操作(动作)的标记。 IO类型是抽象的:用户看不到任何构造函数。 IOMonadFunctor类的实例。

    关键点在于:

    IO类型是抽象的:用户看不到任何构造函数。

    没有构造函数?那就引出了下一个问题:

  • 这是如何工作的?

    这就是单子接口的多功能性发挥作用的地方:Haskell中其两个关键运算符的灵活性-return(>>=)-大大弥补了基于IO的值是抽象的这一缺陷。

    还记得关于如何使用基于Maybe的值的观察吗?好吧,基于IO的值也是以类似的方式使用的-从简单开始(例如return 1getCharputStrLn "Hello, there!"),然后使用(>>=)(>>)catch等定义其他基于IO的值,最终形成Main.main

    但是,与模式匹配或调用另一个函数来提取其内容不同,Main.main直接由Haskell实现处理。

  • 函数中使用的IO期望什么或返回什么?

    对于期望基于IO的值的函数,例如:

    echo :: IO ()
    echo :: getChar >>= \c -> if c == '\n'
                              then return ()
                              else putChar c >> echo 
    

    如果在函数中使用了它的内容,则该函数通常返回基于IO的值。

    对于返回基于IO的值的函数,例如:

    newLine :: IO ()
    newLine  = putChar '\n'
    

    它只需要使用适当的定义即可。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接