Haskell中print <$> readConfig无输出且无-Wall警告的原因是什么?
1. 出错行的类型推导
你遇到的问题核心是Haskell中IO值的执行规则与Functor实例的行为组合导致的,我们先拆解有问题的代码行:print <$> readConfig c
各部分的类型如下:
readConfig c的类型是IO Int,代表一个「执行后返回Int值的IO动作」print的类型是Show a => a -> IO (),接收一个可打印值,返回「执行后输出内容的IO动作」<$>是Functor的fmap运算符,作用在IO的Functor实例上时,它的行为是:执行原IO动作拿到返回值,把返回值传入映射函数,返回新IO动作的结果,但不会执行映射函数返回的IO动作。
所以整行的类型最终是 IO (IO ()):这是一个IO动作,它执行时会先跑readConfig c拿到Int值,把Int包进print得到类型为IO ()的打印动作,然后把这个打印动作作为返回值抛出来。
2. 为什么没有输出?
在do块中,单独一行m a类型的表达式会被自动转化为m a >> return (),也就是执行m a,然后直接丢弃它的返回值,返回单位值()。
对应到你的代码,就是你只执行了外层的IO (IO ())动作,拿到了内层的print对应的IO动作后直接丢弃了,内层的IO动作从来没有被加入到执行流中,自然不会产生任何输出。
你的简化例子里的let t = test也是一样的逻辑:
Haskell中的IO动作本质是普通的一等值,和Int、String这类基础类型的值没有任何区别。let t = test只是给test这个IO动作起了个别名,和let x = 5没有本质区别,只有当你把这个IO值放在do块的顶层作为语句执行时,它才会被真正运行。你要是在let t = test下方加一行单独的t,就会看到对应的输出了。
3. 为什么编译没有警告?
GHC默认不会对「do块中丢弃IO动作返回值」的行为发出警告,因为绝大多数场景下这是合理的(比如putStrLn "xxx"的返回值就是IO (),本来就是要丢弃的)。只有当你主动开启-Wunused-results编译选项时,GHC才会检查到你这里丢弃了一个类型为IO ()的返回值,提示你可能漏了执行。
4. 修复方案
想要让打印动作执行,只需要把嵌套的IO结构展开即可,常见的写法有三种:
- 用
join函数把IO (IO ())压平为IO ():join $ print <$> readConfig c - 直接用Monad的绑定运算符:
readConfig c >>= print - 用do语法显式绑定后执行:
d <- readConfig c print d
内容的提问来源于stack exchange,提问作者blauerreimers

