如何用更单子化的方式实现Haskell函数(a -> m (Either e b)) -> Either e a -> m (Either e b)?
Haskell函数的单子化重构与ExceptT实现
原始函数与问题
现有通过模式匹配实现的函数:
{-# LANGUAGE LambdaCase #-} calculate :: (Monad m) => (a -> m (Either e b)) -> Either e a -> m (Either e b) calculate f = \case Left err -> return $ Left err Right vals -> f vals
问题:
- 能否不用模式匹配,以更“单子化”的方式编写该函数?
- 实际场景中
m为IO类型,f是接收输入后产生IO副作用或执行失败的函数,输入本身也可能失败,是否可以用ExceptT实现?
不用模式匹配的单子化实现
完全可以,利用Either的Traversable实例,直接用traverse函数替代模式匹配:
calculate :: (Monad m) => (a -> m (Either e b)) -> Either e a -> m (Either e b) calculate = traverse
逻辑说明
traverse的类型为Traversable t => (a -> m b) -> t a -> m (t b),这里t对应Either e。当传入f时:
- 若输入是
Left err,traverse会直接返回return (Left err),和原函数逻辑一致; - 若输入是
Right val,则调用f val,同样匹配原函数行为。
整个过程不需要手动写模式匹配,完全利用Traversable的抽象能力实现单子化逻辑。
基于ExceptT的实现
针对m=IO的场景,ExceptT e IO正是为“带IO副作用的可能失败计算”设计的抽象,非常适合你的需求。
重构思路
首先可以把原有的f :: a -> IO (Either e b)转换成ExceptT风格的函数:f' :: a -> ExceptT e IO b,转换方式很简单:f' = ExceptT . f。
然后可以用两种方式实现目标逻辑:
方式1:利用单子组合子(无模式匹配)
import Control.Monad.Trans.Except import Control.Monad ((>=>)) calculateExcept :: (a -> ExceptT e IO b) -> Either e a -> ExceptT e IO b calculateExcept f = except >=> f
except函数负责把Either e a包装成ExceptT e IO a;>=>是单子组合子,将except和f组合:如果包装后的ExceptT是失败态,直接传递错误;如果是成功态,就执行f处理输入。
方式2:显式模式匹配(更直观)
如果需要更直观的写法,也可以用ExceptT的错误抛出函数throwE:
import Control.Monad.Trans.Except calculateExcept :: (a -> ExceptT e IO b) -> Either e a -> ExceptT e IO b calculateExcept f = \case Left err -> throwE err Right val -> f val
转换回IO (Either e b)
如果需要回到原始的IO类型,只需要调用runExceptT即可:
-- 假设input是Either e a类型,f'是ExceptT风格的函数 result :: IO (Either e b) result = runExceptT $ calculateExcept f' input
内容的提问来源于stack exchange,提问作者gust
相关产品推荐
相关产品推荐

