Haskell中如何根据typeclass具体类型参数化返回类型
你当前定义了如下两个typeclass的模板代码:
data TheoryParseErr = TheoryParseErr deriving (Show) class Theory t where applySemantics :: (Functor f, TheorySymbol s) => t -> f a -> f (Either TheoryParseErr s) class TheorySymbol a where readT :: String -> Either TheoryParseErr a
你需要为这些typeclass实现多个实例,以未解释函数(Uninterpreted Functions)场景为例,实例定义代码如下:
data UfTheory = UfTheory deriving (Show, Eq) instance Theory UfTheory where applySemantics _ = fmap readT data UfSymbol = Uninterpreted String deriving (Show) instance TheorySymbol UfSymbol where readT s = Right $ Uninterpreted s
上述代码无法正常编译,核心原因是applySemantics的方法签名中,参数类型a、返回值里的符号类型s都是全局多态的:签名要求对任意满足约束的f、任意实现了TheorySymbol的s、任意输入类型a,函数都能工作,根本没法在具体实例里绑定到UfSymbol这类固定类型上。
你的需求完全可以实现,有两种主流可行写法,可根据场景选择:
方案1:使用关联类型族(Type Family,最贴合需求)
这是Haskell中处理「选定类型类实例时,固定关联的其他类型」的标准方案,直接把Theory类对应的输入元素类型、符号类型作为类的关联类型声明,实例里直接指定具体类型即可。
修改后的可编译代码:
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleContexts #-} data TheoryParseErr = TheoryParseErr deriving (Show) -- 把和具体Theory实例绑定的类型,声明为类的关联类型 class Theory t where -- 每个t实例对应固定的输入元素类型 Elem t type Elem t :: * -- 每个t实例对应固定的符号类型 Sym t,必须满足TheorySymbol约束 type Sym t :: * applySemantics :: (Functor f, TheorySymbol (Sym t)) => t -> f (Elem t) -> f (Either TheoryParseErr (Sym t)) class TheorySymbol a where readT :: String -> Either TheoryParseErr a -- 未解释函数场景的实例实现 data UfTheory = UfTheory deriving (Show, Eq) instance Theory UfTheory where -- 指定UfTheory对应的输入元素是String type Elem UfTheory = String -- 指定UfTheory对应的符号类型是UfSymbol type Sym UfTheory = UfSymbol applySemantics _ = fmap readT data UfSymbol = Uninterpreted String deriving (Show) instance TheorySymbol UfSymbol where readT s = Right $ Uninterpreted s
该写法下,后续给其他理论(比如整数算术、数组理论)写实例时,只需要在实例里给Elem t和Sym t绑定对应具体类型即可,完全匹配「选定t的具体类型时,固定a和s类型」的需求。
方案2:多参数类型类 + 函数依赖(MultiParamTypeClasses + FunctionalDependencies)
如果习惯函数依赖的写法,也可以把Theory定义成多参数类型类,加函数依赖指定t唯一确定a和s:
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE FlexibleInstances #-} data TheoryParseErr = TheoryParseErr deriving (Show) class TheorySymbol a where readT :: String -> Either TheoryParseErr a -- 三个参数:理论类型t、输入元素类型a、符号类型s,t唯一确定a和s class (TheorySymbol s) => Theory t a s | t -> a, t -> s where applySemantics :: Functor f => t -> f a -> f (Either TheoryParseErr s) -- 未解释函数场景实例 data UfTheory = UfTheory deriving (Show, Eq) data UfSymbol = Uninterpreted String deriving (Show) instance TheorySymbol UfSymbol where readT s = Right $ Uninterpreted s instance Theory UfTheory String UfSymbol where applySemantics _ = fmap readT
该写法也能实现需求,缺点是类型类参数变多,调用applySemantics时偶尔需要手动加类型注解帮编译器推断类型,日常使用关联类型族的写法更简洁。
你原来的签名本质是要求函数具备如下高阶多态行为:
不管调用者传什么类型的
f a、要求返回什么TheorySymbol s类型的结果,applySemantics都得能处理
但你的未解释函数实例里只实现了输入是String、输出是UfSymbol的逻辑,覆盖不了所有可能的a和s,自然不符合类型签名的要求,编译器会直接报错。
内容的提问来源于stack exchange,提问作者gust

