如何用无do语法的方式组合Haskell中的IO包裹函数?
你现在有一段用do语法实现的获取用户主目录的IO函数:
getUserHome :: IO String getUserHome = do usr_id <- getRealUserID homeDirectory <$> getUserEntryForID usr_id
想换成不用do的简洁写法,用普通函数组合符.不行,试了<$>却碰到类型不匹配错误:
src/Main.hs:49:21: error: • Couldn't match type ‘IO UserEntry’ with ‘UserEntry’ Expected type: UserID -> UserEntry Actual type: UserID -> IO UserEntry • In the second argument of ‘(<$>)’, namely ‘getUserEntryForID’ In the first argument of ‘(<$>)’, namely ‘homeDirectory <$> getUserEntryForID’ In the expression: homeDirectory <$> getUserEntryForID <$> getRealUserID -- usr_id
错误原因
<$>是Functor的fmap操作,只能把纯函数(输入普通值、输出普通值)映射到IO容器上。但getUserEntryForID返回的是UserID -> IO UserEntry,不是纯函数——它输出的是IO包裹的值。你写的homeDirectory <$> getUserEntryForID得到的是UserID -> IO String,再用<$>去接getRealUserID(IO UserID),相当于把一个返回IO的函数塞进IO容器里,最终会得到IO (IO String),和你需要的IO String类型完全不匹配,所以报错。
正确的Monad组合写法
要处理返回Monad(这里是IO)的函数,得用Monad专属的组合符:
1. 用>>=(绑定操作符)直接替代do逻辑
>>=的作用是把IO包裹的值,传给一个接受普通值、返回IO值的函数,完全对应do里的<-绑定逻辑:
getUserHome :: IO String getUserHome = getRealUserID >>= \usr_id -> homeDirectory <$> getUserEntryForID usr_id
2. 用=<<(反向绑定)+ 函数组合
=<<是>>=的反向写法,把函数放在前面、IO值放在后面,配合.组合纯函数和IO函数:
getUserHome :: IO String getUserHome = (homeDirectory <$>) . getUserEntryForID =<< getRealUserID
这里(homeDirectory <$>)把纯函数homeDirectory提升成能处理IO UserEntry的函数,再和getUserEntryForID组合成UserID -> IO String,最后用=<<绑定到getRealUserID的结果上。
3. 用<=<(Kleisli组合子)专门组合Monad函数
<=<是专门用来组合返回Monad的函数的工具,它能把a -> m b和b -> m c的函数组合成a -> m c,刚好适配你的场景:
import Control.Monad ( (<=<) ) getUserHome :: IO String getUserHome = ((homeDirectory <$>) <=< getUserEntryForID) =<< getRealUserID
或者更简洁的写法:
getUserHome :: IO String getUserHome = getRealUserID >>= (getUserEntryForID <=< (homeDirectory <$>))
内容的提问来源于stack exchange,提问作者Jakob Sachs

