如何为通用Alternative类型实现类似构造函数的模式匹配?
你遇到的问题很典型——Haskell的模式匹配只能针对数据构造器,而empty是Alternative类型类提供的方法,不是构造器,所以直接写f empty = False会报错。下面我会先解决你提出的通用f函数的问题,再针对你实际的自定义结果类型实例给出优化方案。
一、实现通用Alternative类型的“空值检测”
要区分Alternative实例的空值和非空值,不能用模式匹配,得利用Alternative的类型类特性。这里有几种惯用的实现方式:
1. 利用optional函数(最通用)
Control.Applicative里的optional函数可以把任意Alternative值转换成Maybe的包装:
- 当输入是
empty时,返回pure Nothing - 当输入是非空值时,返回
pure (Just x)
我们可以结合Monad提取结果来判断是否为空:
import Control.Applicative (optional) import Data.Maybe (isNothing) isEmpty :: (Alternative m, Monad m) => m a -> Bool isEmpty x = isNothing =<< optional x
如果不想依赖Monad,也可以用foldMap结合All monoid(需要Foldable实例支持):
import Control.Applicative (Alternative) import Data.Monoid (All(..)) import Data.Foldable (foldMap) isEmpty :: (Alternative m, Foldable m) => m a -> Bool isEmpty = getAll . foldMap (\_ -> All False)
这个版本的逻辑是:空的Alternative实例(比如empty)foldMap后得到mempty(即All True),非空值则会把每个元素映射成All False,最终结果为All False。
有了isEmpty,你想要的f函数就可以写成:
f :: (Alternative m, Foldable m) => m a -> Bool f x | isEmpty x = False | otherwise = True
2. 基于Eq实例的简单判断(局限性)
如果你的Alternative实例有Eq实例,可以直接比较是否等于empty:
isEmpty :: (Alternative m, Eq (m a)) => m a -> Bool isEmpty x = x == empty
但这种方法不通用,比如很多Alternative实例(比如IO)没有Eq实例,所以不推荐作为通用方案。
二、针对自定义结果类型的Monad实例优化
你的实际目标是定义一个包含错误类型(Alternative mErr)和结果类型(Applicative mRes)的复合类型的Monad实例,这里有几个关键的优化点:
1. 先包装成新类型(元组不能直接定义实例)
Haskell不允许直接为元组类型定义类型类实例(除非用扩展,但不推荐),所以首先要把元组包装成新类型:
newtype AppResult mErr mRes e a = AppResult (mErr e, mRes a) deriving (Functor, Applicative) -- 可以自动派生基础实例
2. 修正Monad实例的依赖
你提到mRes是Applicative,但Monad的>>=操作需要从mRes a中提取a的值,这要求mRes必须是Monad(因为Applicative没有提取值的能力)。如果确实需要惰性求值,Monad完全可以满足,比如[]或者IO都是惰性的。
3. 正确实现Monad实例
结合前面的isEmpty函数,我们可以实现>>=:当错误部分是empty时,说明没有错误,执行后续的绑定;否则保留错误:
instance (Alternative mErr, Monad mRes, Foldable mErr) => Monad (AppResult mErr mRes e a) where return x = AppResult (empty, pure x) AppResult (err, res) >>= f = case isEmpty err of True -> res >>= \x -> f x -- 无错误,执行绑定 False -> AppResult (err, res) -- 有错误,直接返回
另外,如果你想实现MonadPlus实例,也可以利用Alternative的特性:
instance (Alternative mErr, MonadPlus mRes) => MonadPlus (AppResult mErr mRes e a) where mzero = AppResult (empty, mzero) AppResult (err1, res1) `mplus` AppResult (err2, res2) = AppResult (err1 <|> err2, res1 `mplus` res2)
总结
- 不要试图对
empty进行模式匹配,而是用optional或foldMap实现空值检测 - 自定义复合类型时,一定要先包装成新类型再定义实例
- 注意
Monad实例对结果类型的要求(必须是Monad而非仅Applicative)
内容的提问来源于stack exchange,提问作者Uroc327

