Haskell和惰性Monad求值

8

在学习单子时,我经常遇到评估问题。我现在理解了惰性评估的基本概念,但我不知道Haskell中单子如何进行惰性评估。

考虑以下代码

module Main where
import Control.Monad
import Control.Applicative
import System

main = print <$> head <$> getArgs

在我看来,主要函数应该打印第一个控制台参数,但它没有打印出来。
我知道
getArgs :: IO [String]
head <$> getArgs :: IO String
print <$> (head <$> getArgs) :: IO (IO ())
main :: IO (IO ())

显然,第一个参数没有被打印到标准输出中,因为第一个IO单子的内容没有被评估。所以如果我将这两个单子合并,它就可以工作了。

main = join $ print <$> head <$> getArgs

有人可以为我澄清一下吗?(或者给我一个指示)
2个回答

11
《Haskell 2010 Report》(即语言定义)指出
程序的值是模块Main中标识符main的值,它必须是某种类型τIO τ计算。当程序被执行时,将执行计算main,并且其结果(类型为τ)将被丢弃。
您的main函数的类型是IO (IO ())。上面的引用意味着仅评估外部操作(IO (IO ())),并且其结果(IO ())将被丢弃。您是如何到达这里的?让我们看一下print <$>的类型:
> :t (print <$>)
(print <$>) :: (Show a, Functor f) => f a -> f (IO ())

所以问题在于您将fmapprint一起使用。查看IOFunctor实例定义:
instance  Functor IO where
   fmap f x = x >>= (return . f)

你可以看到,这使得你的表达式等价于 (head <$> getArgs >>= return . print)。要实现你最初的意图,只需删除不必要的return即可。
head <$> getArgs >>= print

或者等价地:

print =<< head <$> getArgs

请注意,Haskell中的IO操作与其他值一样 - 它们可以传递给函数并从函数返回,存储在列表和其他数据结构中等。除非它是主计算的一部分,否则不会评估IO操作。要“粘合”IO操作,请使用>>>>=,而不是fmap(通常用于在某个“盒子”中映射纯函数的值 - 在您的情况下,是IO)。
还要注意,这与惰性评估无关,而与纯度有关 - 从语义上讲,您的程序是一个返回类型为IO a的纯函数,然后由运行时系统进行解释。由于您的内部IO操作不是此计算的一部分,因此运行时系统将其丢弃。有关这些问题的良好介绍是Simon Peyton Jones的《"Tackling the Awkward Squad"》第二章。

非常感谢您详尽的回复。 - Jack
基本上我所做的就是执行 getArgs(外部 IO),并将结果抛到一个从未被执行的东西中(因为被主函数忽略了)。 - Jack
1
正确。您的代码执行 head <$> getArgs,然后将其结果提供给 return . print,它的类型为 a -> IO (IO ()) - Mikhail Glushenkov
1
虽然 head 没有被评估,因为它的结果不需要,所以你的代码永远不会产生“空列表”异常。你可以通过将 getArgs 更改为 getLine 来测试这一点。 - Mikhail Glushenkov

4
你有一个head <$> getArgs :: IO Stringprint :: Show a => a -> IO (),即一个来自单子的值和一个从普通值到单子的函数。用于组合这些东西的函数是单子绑定运算符(>>=) :: Monad m => m a -> (a -> m b) -> m b
因此,你想要的是:
main = head <$> getArgs >>= print

`(<$>)`,也称为 `fmap`,其类型为 `Functor f => (a -> b) -> f a -> f b`。因此,在您想要将一个“纯函数”应用于单子中的某个值时,它非常有用,这就是为什么它与 `head` 一起使用而不能与 `print` 一起使用的原因,因为 `print` 不是纯的。

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