在函子的世界中,有两个非常有趣的函子:Identity和Const,它们代表了容器概念的两个极端。我们先来看Identity,它代表最简单的容器,相当于把Maybe中的Nothing去掉,只有一个构造函数来把值装到盒子里。考虑到newtype语法可以避免底层的打包/解包,我们这么定义Identity:

newtype Identity a = Identity { runIdentity :: a }

Identity 3 :: Identity Int

runIdentity $ Identity 3
-- 3

我们使用记录语法定义了一个只有一个参数的构造函数Identity来把值装进盒子里,然后提供了runIdentity函数方便我们把值从盒子中取出。现在需要提供Identity的函子类型类的实例声明:

instance Functor Identity where
fmap f idx = Identity (f (runIdentity idx))
-- point-free写法
fmap f = Identity . f . runIdentity
fmap (+1) (Identity 3)
-- Identity 4

这个函子的fmap也相当于没做什么,只是简单地把值从盒子中取出,应用一下f,然后装回到盒子中。这个过程和函数id很像。Identity代表不含任何额外上下文信息的盒子。试着用上面说到的函子需要满足的两个条件,看看Identity是否满足:

fmap id (Identity 3) == id (Identity 3)
fmap ((+1) . (*2)) (Identity 3) == (fmap (+1) . fmap (*2)) (Identity 3)

很容易看出,这两个条件是满足的。我们再来看看另外一个更加奇特的函子Const:

newtype Const a b = Const { getConst :: a }
Const 3 :: Const Int String
Const 3 :: Const Int Int
...
getConst (Const 3)
-- 3 

它看起来和Identity很像,其构造函数也只有一个参数,所以我们同样使用newtype的把戏来优化代码的速度。至于为什么这里用get...来提取,而上面用run...,只能说是个不幸的命名事故。有趣的地方在于Const 3构造出来的值的类型是Const Int a,这个额外的类型变量在之前提到过,允许Const 3成为各种类型:Const Int Int、Const Int String等。如果newtype或者data声明时,=左边出现的类型变量没有出现在右边,就把这样定义出来的类型叫作幻影类型(phantom type)。例如,上面的Const就是一个幻影类型。幻影类型有趣的地方在于,对于一个幻影类型的值来说,例如Const 3,它的具体类型是不确定的,但是通过幻影类型本身携带的额外的类型变量,编译器却可以区分相同的两个值:

Prelude> import Control.Applicative
Prelude Control.Applicative> Const 3 == Const 3
True
Prelude Control.Applicative> (Const 3 :: Const Int Int) == (Const 3 :: Const Int String)

<interactive>:5:32:
Couldn't match type ‘[Char]’ with ‘Int’
Expected type: Const Int Int
Actual type: Const Int String
In the second argument of ‘(==)’, namely
‘(Const 3 :: Const Int String)’
In the expression:
(Const 3 :: Const Int Int) == (Const 3 :: Const Int String)
In an equation for ‘it’:
it = (Const 3 :: Const Int Int) == (Const 3 :: Const Int String) 

这里我们先导入定义了Const的模块Control.Applicative,然后试着比较一下Const 3。当不提供类型说明的时候,编译器推断它们的类型一定是相同的,不然无法比较;但是当我们试着提供不同的类型注释时,我们发现在编译器看来,Const 3 :: Const Int Int和Const 3 :: Const Int String由于类型不同,是无法比较的。

这也是为什么用type无法定义幻影类型。因为type定义的类型仅仅是类型别名罢了。假如我们有下面的别名:

 type Foo a = Int
 (3 :: Foo String) == (3 :: Foo Char)
-- (3 :: Int) == (3 :: Int)
-- True

在类型检查的第一步,这些类型别名就会被替换成真正的类型,所以在编译器看来,Foo String和Foo Char都是同一个类型,它们不过是Int的别名罢了。

搞懂了幻影类型之后,你一定会问:为什么需要这么奇怪的一个类型Const a b?这个问题在下一章中就会解释。我们需要关注的是Const a b类型中,我们认为b类型的值是盒子里的装载,而a类型的值是盒子的一部分,而构造函数并不需要提供b类型的参数。因为我们根本不关心盒子里装的是什么,构造Const a b时也根本没有往里面装任何东西,我们关心的是写在盒子上面的a的值。下面来看一下Const a的函子类型类实例声明:

instance Functor (Const a) where
fmap f c = c

这里的fmap的类型是fmap :: (b -> c) -> Const a b -> Const a c,我们把Const a当成一整个盒子。因为我们不关心盒子里装着什么,所以直接把接到b -> c类型的函数f丢弃,返回接收到的c。实际上,这里把c的类型从Const a b转换到了Const a c,而对于c来说,这个值的类型可以是任意的Const a x,而不需要对c做任何处理。

Const a代表了盒子抽象的另一个极端。这是一个什么都没有装的空盒子,通过幻影类型,Const a b给你产生了盒子里装有一个b类型的值的幻觉,但实际上这个值根本不存在,而对这个盒子做fmap的结果相当于什么都不做,原封不动地把盒子返回即可。有兴趣的读者可以验证下,它是否满足函子的两个约束呢?

评论

本文目前还没有评论……

我要评论

需要登录后才能发言
登录未成功,请修改提交。

× 450
× 1756
× 2435
× 910
× 1
× 1
× 1189
× 0
× 1
× 0
× 2
× 1
× 3
× 3
× 2745
× 817
× 1097