如何将多参数类型嵌入带类约束的单一类型容器?
我想创建一个能对多种数据类型执行操作的Haskell类,比如给Map结构的所有元素加一个值并返回新Map。我用MultiParamTypeClasses和FlexibleInstances扩展实现了OPER类及实例:
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-} import qualified Data.Map as Map class OPER a b c where add:: a -> b -> c instance OPER (Maybe Double) (Maybe Double) (Maybe Double) where add (Just a) (Just b) = Just (a + b) add _ _ = Nothing instance OPER (Map.Map Int (Maybe Double)) (Maybe Double) (Map.Map Int (Maybe Double)) where add ma mbdlb = Map.map (\mbi -> add mbi mbdlb ) ma
这段代码运行正常,测试代码:
main = do print $ (add (Just (3.5::Double)) (Just (9.7::Double)) :: (Maybe Double)) print $ (add (Map.fromList [(1,Just 3.4),(2,Just 9.4),(3::Int,Just (9.7::Double)),(4,Just 4.8)]) (Just (9.7::Double)) :: (Map.Map Int (Maybe Double)))
输出符合预期:
Just 13.2 fromList [(1,Just 13.1),(2,Just 19.1),(3,Just 19.4),(4,Just 14.5)]
现在我希望把所有支持OPER的类型嵌入一个统一的AnyData类型,定义如下:
data AnyData = forall s . (Show s, Eq s) => DAT s
并尝试为AnyData实现OPER实例:
instance Show AnyData where show (DAT a) = "DAT "++show a instance OPER AnyData AnyData AnyData where add (DAT a) (DAT b) = DAT (add a b)
但编译时出现错误:
• Could not deduce (OPER s s1 s0) arising from a use of ‘add’ from the context: (Show s, Eq s) bound by a pattern with constructor: DAT :: forall s. (Show s, Eq s) => s -> AnyData, in an equation for ‘add’ at testMultiFun_test5.hs:40:8-12 or from: (Show s1, Eq s1) bound by a pattern with constructor: DAT :: forall s. (Show s, Eq s) => s -> AnyData, in an equation for ‘add’ at testMultiFun_test5.hs:40:16-20 The type variable ‘s0’ is ambiguous Relevant bindings include b :: s1 (bound at testMultiFun_test5.hs:40:20) a :: s (bound at testMultiFun_test5.hs:40:12) • In the first argument of ‘DAT’, namely ‘(add a b)’ In the expression: DAT (add a b) In an equation for ‘add’: add (DAT a) (DAT b) = DAT (add a b) | 40 | add (DAT a) (DAT b) = DAT (add a b) | ^^^^^^^
我尝试修改AnyData的定义,比如:
data AnyData = forall s . (OPER s s s, Show s, Eq s) => DAT s
或者:
data AnyData = forall s s1 s2 . (OPER s s1 s2) => DAT s
还启用了AllowAmbiguousTypes,甚至简化为:
data AnyData = forall s . DAT s
但始终遇到类似错误:
• No instance for (OPER s s1 s0) arising from a use of ‘add’ • In the first argument of ‘DAT’, namely ‘(add a b)’ In the expression: DAT (add a b) In an equation for ‘add’: add (DAT a) (DAT b) = DAT (add a b) | 43 | add (DAT a) (DAT b) = DAT (add a b) |
我理解编译器无法推导AnyData下add对应的具体类型,请问有没有办法把所有支持OPER的类型嵌入单一类型(比如生成结果列表)?有没有其他实现方式,比如GADTs或类型族?
核心问题在于存在类型(existential type)擦除了具体类型信息:编译器无法从AnyData的两个值(DAT a和DAT b)推导出它们之间存在OPER s s1 s0的实例关系。你的AnyData只保存了值和Show/Eq约束,没有携带OPER相关的类型关联信息——编译器不知道a和b应该用哪一组OPER实例来执行add,也不知道add a b的结果类型是什么,自然无法包装成DAT构造器。
下面提供几种可行的实现方式:
1. 给存在类型绑定OPER约束并统一操作模式
如果你的add操作主要是同类型相加得到同类型(比如Maybe Double + Maybe Double → Maybe Double),可以调整AnyData的定义,绑定OPER s s s约束,同时用Typeable检查类型一致性:
{-# LANGUAGE ExistentialQuantification, MultiParamTypeClasses, FlexibleInstances, Typeable #-} import qualified Data.Map as Map import Data.Typeable class OPER a b c where add :: a -> b -> c instance OPER (Maybe Double) (Maybe Double) (Maybe Double) where add (Just a) (Just b) = Just (a + b) add _ _ = Nothing instance OPER (Map.Map Int (Maybe Double)) (Maybe Double) (Map.Map Int (Maybe Double)) where add ma mbdlb = Map.map (\mbi -> add mbi mbdlb) ma -- 调整AnyData,绑定OPER s s s约束 data AnyData = forall s. (OPER s s s, Show s, Eq s, Typeable s) => DAT s instance Show AnyData where show (DAT a) = "DAT " ++ show a -- 实现OPER AnyData AnyData AnyData instance OPER AnyData AnyData AnyData where add (DAT a) (DAT b) = case cast b of Just b' -> DAT (add a b') Nothing -> error "Cannot add incompatible AnyData types"
这里用cast检查两个AnyData的底层类型是否一致,只有同类型才能执行add,否则抛出错误。编译器就能确定add a b'符合OPER s s s的约束,结果类型也是s,可以包装成DAT。
2. 使用GADTs明确类型关联
GADTs可以让存在类型携带更精确的类型约束和关联信息,明确存储的类型支持和某个特定类型相加得到另一个类型:
{-# LANGUAGE GADTs, MultiParamTypeClasses, FlexibleInstances #-} import qualified Data.Map as Map class OPER a b c where add :: a -> b -> c instance OPER (Maybe Double) (Maybe Double) (Maybe Double) where add (Just a) (Just b) = Just (a + b) add _ _ = Nothing instance OPER (Map.Map Int (Maybe Double)) (Maybe Double) (Map.Map Int (Maybe Double)) where add ma mbdlb = Map.map (\mbi -> add mbi mbdlb) ma -- GADT定义,明确存储的类型s支持和t相加得到u data AnyOper s t u where AnyOper :: (OPER s t u, Show u, Eq u) => s -> AnyOper s t u -- 针对AnyOper实现OPER instance OPER (AnyOper s t u) t (AnyOper u t u) where add (AnyOper s) t = AnyOper (add s t) -- 示例使用 main = do let val1 = AnyOper (Just 3.5 :: Maybe Double) val2 = Just 9.7 :: Maybe Double result = add val1 val2 print result -- 输出 AnyOper (Just 13.2)
这种方式类型安全,编译器能在编译期检查操作的类型兼容性,但AnyOper的类型参数会暴露底层的类型关联,无法完全隐藏具体类型。
3. 封装操作到存在类型内部
把add操作直接封装在AnyData中,让存在类型内部携带操作逻辑,避免编译器推导类型:
{-# LANGUAGE ExistentialQuantification, Typeable #-} import qualified Data.Map as Map import Data.Typeable class OPER a b c where add :: a -> b -> c instance OPER (Maybe Double) (Maybe Double) (Maybe Double) where add (Just a) (Just b) = Just (a + b) add _ _ = Nothing instance OPER (Map.Map Int (Maybe Double)) (Maybe Double) (Map.Map Int (Maybe Double)) where add ma mbdlb = Map.map (\mbi -> add mbi mbdlb) ma -- 封装add操作的存在类型 data AnyData = forall s t u. (OPER s t u, Show u, Eq u, Typeable s, Typeable t) => DAT s (t -> u) -- 直接调用内部的add逻辑 addAny :: AnyData -> AnyData -> Maybe AnyData addAny (DAT s addFn) (DAT t _) = case cast t of Just t' -> Just (DAT (addFn t') (\x -> add (addFn t') x)) Nothing -> Nothing instance Show AnyData where show (DAT s _) = "DAT " ++ show s -- 示例使用 main = do let val1 = DAT (Just 3.5 :: Maybe Double) (\x -> add (Just 3.5) x) val2 = DAT (Just 9.7 :: Maybe Double) (\x -> add (Just 9.7) x) case addAny val1 val2 of Just res -> print res -- 输出 DAT Just 13.2 Nothing -> putStrLn "Incompatible types"
这种方式把操作逻辑和数据一起存储,避免了类型推导问题,但需要手动封装每个值的add函数,代码略显繁琐。
内容的提问来源于stack exchange,提问作者JeanJouX

