纯函数中的IO操作是什么意思?

11

我曾经认为在Haskell的类型系统中,纯函数无法调用不纯函数(例如 f :: a -> IO b),但今天我意识到,通过使用return 调用它们,这样做是可以编译通过的。在这个例子中:

h :: Maybe ()
h = do
    return $ putStrLn "???"
    return ()

hMaybe 函子中工作,但它仍然是一个纯函数。编译和运行它只会返回 Just (),就像人们所期望的那样,而不实际执行任何 I/O 操作。我认为 Haskell 的惰性将这些东西放在一起(即 putStrLn 的返回值未被使用 - 由于其值构造函数已隐藏且无法模式匹配,请问), 但为什么这段代码是合法的?是否有其他原因使其被允许?

额外问题:通常来说,在一个函子中禁止另一个函子执行操作是否可能,如何实现?

2个回答

22

IO操作和其他任何一种值一样,这就是使Haskell的IO如此表达力的原因,允许您从头开始构建高阶控制结构(例如mapM_)。这里与惰性无关,只是您实际上没有执行该操作。你只是构造了值Just (putStrLn"???"),然后将其丢弃。

putStrLn"???"存在并不会导致屏幕上打印出一行文字。单独的putStrLn"???"只是对可以执行打印一行文字这个IO操作的描述。唯一发生的执行是执行您从其他IO操作构建的main,或者您在GHCi中键入的任何操作。有关更多信息,请参见IO介绍

确实,您可能希望在Maybe内部处理IO操作;想象一个函数String -> Maybe (IO ()),它检查字符串是否有效,如果有效,则返回执行打印一些字符串派生信息的IO操作。这正是由于Haskell的IO操作是第一类对象,所以这种情况是可行的。

但是,除非您赋予其能力,否则单个monad无法执行另一个monad的操作。

1的确,h = putStrLn "???" `seq` return ()也不会执行任何IO操作,即使它强制评估putStrLn"???"


我如何赋予一个单子执行另一个单子的能力,然后通过赋予它针对其包含的值进行模式匹配的可能性? - Riccardo T.
3
通过编写将一个monad转换为另一个monad的方法,或执行某些操作。例如,Control.Monad.ST.stToIO 可以将 ST 计算转换为 IO 计算。 - Louis Wasserman
感谢“只是io的描述”的解释。这让我对io的疑惑有了答案,甚至不需要关心单子。 - masterxilo
谢谢您的回答。我在想像 f :: (Int -> IO ()) -> Int 这样的函数会发生什么。 - funct7

5
让我们进行去糖化操作!
h = do return (putStrLn "???"); return ()
-- rewrite (do foo; bar) as (foo >> do bar)
h = return (putStrLn "???") >> do return ()
-- redundant do
h = return (putStrLn "???") >> return ()
-- return for Maybe = Just
h = Just (putStrLn "???") >> Just ()
-- replace (foo >> bar) with its definition, (foo >>= (\_ -> bar))
h = Just (putStrLn "???") >>= (\_ -> Just ())

现在,当您评估h时会发生什么?对于Maybe来说,
(Just x) >>= f = f x
Nothing  >>= f = Nothing

所以我们对第一个情况进行模式匹配。
f x
-- x = (putStrLn "???"), f = (\_ -> Just ())
(\_ -> Just ()) (putStrLn "???")
-- apply the argument and ignore it
Just ()

请注意,我们无需执行putStrLn "???"就能评估此表达式。
*注意,在“语法糖”的哪个点停止并开始“评估”有些不清楚。这取决于编译器的内联决策。纯计算可以完全在编译时评估。

1
感谢您进行解糖操作。对于新手来说非常有用。我不明白为什么这么多教程都从语法糖开始讲解。先吃正餐,再享受甜点。 - masterxilo

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