简单代数数据类型的函子实例

Functor instance for a simple algebraic data type

我想使用列表的异构列表。为此,我定义了一个简单的代数数据类型:

data T = LInt [Int]
       | LChar [Char]
       deriving (Eq, Ord, Show)

所以我现在可以拥有这样的东西:

x = [LInt [1, 2, 3], LChar "test"]

我的问题:这个类型可以成为 Functor 的实例吗?这会很有用;例如 x 中列表的 select 个元素,如

fmap (!!2) LChar "test"  => 's'

在我看来,这是不可能的。除了问题的动机,我相信答案会帮助我更好地理解代数数据类型。

否,因为它没有权限 kind。函子应该有种类 * -> *T 应该有种类 *。仿函数必须是具有单个类型参数的类型构造函数,例如 IOMaybeEither a。这反映在 fmap:

的类型中
fmap :: Functor f => (a -> b) -> f a -> f b

所以 f 需要类型参数 a。您的类型 T.

没有这样的参数

简答:

不行,不能做成Functor

长答案:

不能将 this 变成函数的第一个原因是仿函数必须具有 * -> * 类型。种类就像类型的类型,您甚至可以使用 :kind <type> 在 GHCi 中检查它们。例如:

> :kind Int
Int :: *
> :kind []
[] :: * -> *
> :kind Num
Num :: * -> Constraint
> :kind Maybe
Maybe :: * -> *
> :kind Either
Either :: * -> * -> *
> :kind Functor
Functor :: (* -> *) -> Constraint

*基本上是指完全应用的类型,如IntChar[String]等,而* -> *则表示type 接受一种类型 * 到 return 一种新的类型 *。约束也有种类,即它们 return 完全应用时的种类 Constraint

您的类型有种类 *,它与 Functor 的参数所需的 * -> * 不匹配。为了使其成为 Functor ,它需要接受一个类型变量。在这里添加一个类型变量没有多大意义,但你可以

data T a = LInt [a] | LChar [a]

但这不是很有用,我们现在不能强制 LInt 只包含 IntLChar 只包含 Char。更糟糕的是,看看 fmap 的类型,我们有

class Functor f where
    fmap :: (a -> b) -> (f a -> f b)

但是你想要做的是

myfmap :: (a -> b) -> (f a -> b)

请注意 return 类型是 b 而不是 f bfmap 函数仅转换容器内的值,它不会从所述容器中提取值。

可以编写使用 -XGADTs 约束的参数化类型,不过,您可以编写

data T a where
    LInt :: [Int] -> T Int
    LChar :: [Char] -> T Char

这将保证类型是健全的,但仍然不可能将它变成 Functor 的实例(即满足仿函数法则)并且它会阻止您制作异类列表 (T Int /~ T Char).

所以 Functor 选项看起来真的很合适。您可能会发现编写类似

的函数很诱人
tmap :: ([a] -> b) -> T -> b
tmap f (LInt x) = f x
tmap f (LChar x) = f x

但这也行不通。类型系统看到你想说的是 f :: [Int] -> bf :: [Char] -> b,不能统一。您可以通过启用 -XRankNTypes 来做到这一点:

tmap :: (forall a. [a] -> b) -> T -> b
tmap f (LInt x) = f x
tmap f (LChar x) = f x

这确实允许您执行类似

的操作
> tmap length (LInt [1, 2, 3])
3
> tmap length (LChar "test")
4

但它不会让你这样做

> tmap (!! 2) (LChar "test")
Couldn't match type 'b' with 'a'
  because type variable 'a' would escape its scope
This (rigid, skolem) type variable is bound by
  a type expected by the context: [a] -> b
Expected type: [a] -> b
  Actual type: [a] -> a
...

这意味着类型 a 不能出现在输出类型 b 中的任何地方,因为传入的 f 必须为 工作all a,它不能只对任何 a.

有效

总而言之,无需进一步深入研究疯狂的类型系统,就无法让您的类型按照您的意愿行事。您将不得不编写专门的函数来单独处理每种情况,这几乎就是 ADT 的要点。编译器可以确保您确实处理了每种情况,只要您远离 return undefined 或调用 error 的函数,那么您的程序就是安全的。它可能不像您想要的那样灵活,但在安全方面它会坚如磐石。