我有一个实用程序函数,它枚举了一个可枚举和有界的类型的所有值:
enumerate :: (Enum a, Bounded a) => [a]
enumerate = [minBound .. maxBound]
和涉及将可枚举类型映射到整数的数据类型:
data Attribute a = Attribute { test :: a -> Int
, vals :: [Int]
, name :: String }
其中vals
是表示所有可能的可枚举值的整数列表。例如,如果我有
data Foo = Zero | One | Two deriving (Enum,Bounded)
然后vals
将是[0,1,2]
。
我希望能够以编程方式创建这些属性,只需给出一个将a
映射到可枚举类型和名称的函数。像这样:
attribute :: (Enum b, Bounded b) => (a -> b) -> String -> Attribute a
attribute f str = Attribute (fromEnum . f) vs str
where
vs = map fromEnum enumerate
这不是类型检查,因为无法将enumerate
的调用与类型签名中的b
连接起来。所以我想我能做到这一点:
vs = map fromEnum $ enumerate :: [b]
但是也没有编译 - 编译器将b
重命名为b1
。我试图更聪明,使用GADTs扩展:
attribute :: (Enum b, Bounded b, b ~ c) => {- ... -}
vs = map fromEnum $ enumerate :: (Enum c,Bounded c) => [c]
但同样,c
已重命名为c1
。
我不希望将b
的类型包含在Attribute
类型中作为参数(主要是因为我想存储具有可能不同的b
值的属性列表 - 这就是test
类型为a -> Int
而vals
类型为[Int]
)的原因。
如何编写此代码以使其完成我想要的操作?
答案 0 :(得分:6)
类型变量的问题在于它们仅绑定在类型签名中。在定义中使用类型变量将引用新的,新的类型变量(即使它与类型签名中的名称完全相同)。
有两种方法可以从签名中引用类型变量:ScopedTypeVariables
扩展名和asTypeOf
。
使用ScopedTypeVariables
明确绑定forall
的类型变量也可用于定义,因此:
attribute :: forall a b. (Enum b, Bounded b) =>
(a -> b) -> String -> Attribute a
attribute f str = Attribute (fromEnum . f) vs str
where
vs = map fromEnum (enumerate :: [b])
另一种方式涉及函数asTypeOf
定义为:
asTypeOf :: a -> a -> a
asTypeOf = const
如果我们可以在第二个参数中获得[b]
类型的表达式,则统一将确保第一个参数也具有类型[b]
。因为我们有f :: a -> b
和f undefined :: b
,所以我们可以写:
attribute :: (Enum b, Bounded b) => (a -> b) -> String -> Attribute a
attribute f str = Attribute (fromEnum . f) vs str
where
vs = map fromEnum (enumerate `asTypeOf` [f undefined])