简单代数数据类型的函子实例
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
应该有种类 *
。仿函数必须是具有单个类型参数的类型构造函数,例如 IO
、Maybe
、Either 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
*
基本上是指完全应用的类型,如Int
、Char
、[String]
等,而* -> *
则表示type 接受一种类型 *
到 return 一种新的类型 *
。约束也有种类,即它们 return 完全应用时的种类 Constraint
。
您的类型有种类 *
,它与 Functor
的参数所需的 * -> *
不匹配。为了使其成为 Functor
,它需要接受一个类型变量。在这里添加一个类型变量没有多大意义,但你可以
data T a = LInt [a] | LChar [a]
但这不是很有用,我们现在不能强制 LInt
只包含 Int
和 LChar
只包含 Char
。更糟糕的是,看看 fmap
的类型,我们有
class Functor f where
fmap :: (a -> b) -> (f a -> f b)
但是你想要做的是
myfmap :: (a -> b) -> (f a -> b)
请注意 return 类型是 b
而不是 f b
。 fmap
函数仅转换容器内的值,它不会从所述容器中提取值。
可以编写使用 -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] -> b
和 f :: [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
的函数,那么您的程序就是安全的。它可能不像您想要的那样灵活,但在安全方面它会坚如磐石。
我想使用列表的异构列表。为此,我定义了一个简单的代数数据类型:
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
应该有种类 *
。仿函数必须是具有单个类型参数的类型构造函数,例如 IO
、Maybe
、Either 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
*
基本上是指完全应用的类型,如Int
、Char
、[String]
等,而* -> *
则表示type 接受一种类型 *
到 return 一种新的类型 *
。约束也有种类,即它们 return 完全应用时的种类 Constraint
。
您的类型有种类 *
,它与 Functor
的参数所需的 * -> *
不匹配。为了使其成为 Functor
,它需要接受一个类型变量。在这里添加一个类型变量没有多大意义,但你可以
data T a = LInt [a] | LChar [a]
但这不是很有用,我们现在不能强制 LInt
只包含 Int
和 LChar
只包含 Char
。更糟糕的是,看看 fmap
的类型,我们有
class Functor f where
fmap :: (a -> b) -> (f a -> f b)
但是你想要做的是
myfmap :: (a -> b) -> (f a -> b)
请注意 return 类型是 b
而不是 f b
。 fmap
函数仅转换容器内的值,它不会从所述容器中提取值。
可以编写使用 -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] -> b
和 f :: [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
的函数,那么您的程序就是安全的。它可能不像您想要的那样灵活,但在安全方面它会坚如磐石。