我对Haskell编程语言很陌生,经常在函数参数或返回类型中遇到IO
类型。
playGame :: Screen -> IO ()
或者
gameRunner :: IO String -> (String -> IO ()) -> Screen -> IO ()
这个是怎么工作的,我有点困惑,因为我知道String类型需要单词和Int类型需要数字。在函数中使用的IO
期望或返回什么?
我对Haskell编程语言很陌生,经常在函数参数或返回类型中遇到IO
类型。
playGame :: Screen -> IO ()
或者
gameRunner :: IO String -> (String -> IO ()) -> Screen -> IO ()
这个是怎么工作的,我有点困惑,因为我知道String类型需要单词和Int类型需要数字。在函数中使用的IO
期望或返回什么?
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
类型内部。
f1 :: A -> B -> C
这是一个函数,接受两个类型为A
和B
的参数,并返回一个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 Eshiettf6 (do putStrLn "hi" ; return 12)
。 - chi让我们先尝试回答一些更简单的问题:
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.5
或 Just "here"
),然后定义其他可能更复杂的基于 Maybe
的值(使用 (>>=)
、(>>)
等),最终通过模式匹配直接检查,或通过适当的定义(maybe
、fromJust
等)进行抽象。
原始问题的时间:
Haskell中的IO
类型是什么?
来自报告第6.1.7节(第75页):
IO
类型用作与外部世界交互的操作(动作)的标记。IO
类型是抽象的:用户看不到任何构造函数。IO
是Monad
和Functor
类的实例。
关键点在于:
IO
类型是抽象的:用户看不到任何构造函数。
没有构造函数?那就引出了下一个问题:
这是如何工作的?
这就是单子接口的多功能性发挥作用的地方:Haskell
中其两个关键运算符的灵活性-return
和(>>=)
-大大弥补了基于IO
的值是抽象的这一缺陷。
还记得关于如何使用基于Maybe
的值的观察吗?好吧,基于IO
的值也是以类似的方式使用的-从简单开始(例如return 1
、getChar
或putStrLn "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'
它只需要使用适当的定义即可。