我正在学习haskell,其中一个棘手的部分是类型变量 请考虑以下示例:
Prelude> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
有类型变量a
和b
,它们可以是任何类型。 f
是一种必须实现Functor的类型。
让我们为fmap
的第一个参数定义一个函数:
Prelude> let replaceWithP = const 'p'
现在,我将函数replaceWithP
传递给fmap
并查看类型签名:
Prelude> :t fmap replaceWithP
fmap replaceWithP :: Functor f => f b -> f Char
为什么f a
变为f b
,为什么不归a
?
答案 0 :(得分:3)
首先是类型
fmap replaceWithP :: Functor f => f b -> f Char
完全等同于
fmap replaceWithP :: Functor f => f a -> f Char
因为所有类型变量都是隐式普遍量化的,因此可以随意重命名(这称为alpha转换)。
人们可能仍然想知道GHC打印的名称b
来自哪里。毕竟,fmap
在其类型中有f a
,为什么GHC选择将其重命名为b
?
这里的“罪魁祸首”是replaceWithP
> :t const
const :: a -> b -> a
> let replaceWithP = const 'p'
> :t replaceWithP
replaceWithP :: b -> Char
因此,b
来自replaceWithP
。
答案 1 :(得分:3)
类型变量可以被认为是正常变量,除了你有类型。
这是什么意思?例如,C中的变量a
可能定义为:
int a = 2;
您可以分配a
的可能值是多少?整个int
范围,因为它是a
可能采用的值的集合。让我们在伪Haskell中看一下这个:
type b = Int
b
可能采用的值集是什么?这是一个棘手的问题。通常,我们会习惯将2
,"hello"
或True
等内容视为值。但是,在Haskell中,我们还允许将类型视为值。有点。我们假设b
可以采用任何kind形式*。从本质上讲,这包括所有不需要额外信息的类型:
data Tree a = Leaf a | Branch (Tree a) (Tree a)
Tree -- no, has kind: * -> *
Tree Int -- okay!
Int -- okay!
String -- okay!
这意味着在您的示例中:
fmap :: Functor f => (a -> b) -> f a -> f b
变量a
和b
可以被认为是可以采用任何形式类型的变量,前提是您决定提供它的类型在适当的类型值范围内(受限于种)。
现在更准确地回答你的问题:为什么我们会注意到:
fmap :: Functor f => (a -> b) -> f a -> f b
fmap replaceWithP :: Functor f => f b -> f Char
让我重写以下等效定义,因为变量命名会引起混淆:
fmap :: Functor f => (a -> b) -> f a -> f b
fmap replaceWithP :: Functor f => f z -> f Char
希望现在看起来更加清晰。当您提供replaceWithP :: x -> Char
函数时,会发生以下映射:
-- Function types
fmap :: Functor f => (a -> b) -> f a -> f b
replaceWithP :: x -> Char
-- Type variable mappings
a -> x
b -> Char
如果我们进行替换,这会是什么样的?
Functor f => (x -> Char) -> f x -> f Char
在replaceWithP
函数中提供后,您将使用第一个参数,因此您将离开:
fmap replaceWithP :: Functor f => f x -> f Char
或等效地:
fmap replaceWithP :: Functor f => f b -> f Char