Haskell – 初识 Lens
1. 什么是 Lens
在 Haskell 等不可变语言中,如果我们希望修改数据中的某个值,就需要创建数据的一个新的实例。如我们定义一个名为 Name
的数据结构表示人名:
:set +m data Name = Name { _first :: String , _last :: String } deriving Show
然后定义我们的名字,我们可以通过记录语法提供的 _first
和 _last
函数分别从 Name 对象中获取两个成员,这种形式的函数被称为 getter 函数
name = Name "Cycoe" "Joo" print name
Name {_first = "Cycoe", _last = "Joo"}
如果我们想要方便地对 first name 进行修改,我们可以定义一个 setFirst
工具函数。接收一个 Name 和新的 first name,返回新的 Name 对象,这种形式的函数被称为 setter 函数
我们可以使用这个函数修改我们的 first name 并生成一个新的名字
:{ setFirst :: Name -> String -> Name setFirst (Name _ l) f' = Name f' l :} setFirst name "Handsome"
Name {_first = "Handsome", _last = "Joo"}
那么有没有什么办法可以通过一个统一的数据类型表示 setter 和 getter 函数呢?当然有,那就是已经被发现的 Lens 类型。Lens 含义是透镜,它的功能就是提供对数据结构内部成员进行查看(view)、写入(set)和变换(over)的能力。
2. 定义 Lens 类型
Lens 类型可以被定义为如下的类型
:set -XRankNTypes type Lens s a = forall f. Functor f => (a -> f a) -> s -> f s
Lens 被定义为一个高阶函数,接收一个将成员类型 a
转换为 Functor a
的函数和一个聚合类型 s
,并返回 Functor s
。此处为什么会引入 Functor?到下面我们会逐渐明白
有了 Lens
类型,我们还需要一个工具函数 lens
帮助我们将 setter
和 getter
函数构造成 Lens
lens :: (s -> a) -> (s -> a -> s) -> Lens s a
lens
的类型非常清晰,接收一个 getter
函数和一个 setter
函数并返回构造好的 Lens
对象。但是我们要怎么实现它呢?
既然我们已经知道 Lens
类型是一个函数,那我们可以试着将它展开,得到 lens
函数的真实类型
lens :: (s -> a) -> (s -> a -> s) -> (a -> f a) -> s -> f s lens getter setter f s = ???
我们使用了一些变量表示函数的参数,那么等号的右边又该是什么样的呢?
在实现 Haskell 函数时,一种常用的思路是根据组合的方式拼出需要的类型,再来验证是否正确,此处我们也可以这样做。我们使用 getter
从 s
对象中获取出成员 a
,再使用函数 f
将 a
转换为 Functor a
,最后再将 setter s
作用到函子 Functor a
上得到函子 Functor s
:{ lens :: (s -> a) -> (s -> a -> s) -> Lens s a lens getter setter f s = setter s <$> f (getter s) :}
这样我们就可以构造我们的第一个 Lens
了
:{ firstL :: Lens Name String firstL = lens _first setFirst :} :t firstL
firstL :: Functor f => (String -> f String) -> Name -> f Name
3. view
函数
有了 Lens 之后我们要怎么利用它查看结构体中的成员呢?我们需要定义一个 view
函数,接收一个透镜对象和一个结构体,返回要查看的成员
view :: Lens s a -> s -> a view l s = ???
现在我们有了透镜 l
和结构体 s
,我们需要利用这两个对象构造出类型 a
。我们的透镜是由 lens
函数生成的,因此 l
等价于如下表示,对应到上面 lens
函数的实现
l :: Functor f => (a -> f a) -> s -> f s l f s = setter s <$> f (getter s)
也就是说我们可以通过把函数 f
和结构体 s
传给 l
生成一个 f s
类型的返回值。那么在函数 view
的参数中我们已经有了 l
和 s
,还缺少一个函数 f
将类型 a
转换为函子。此处函子类型的选择是关键,这也是 Lens
类型中引入函子类型的原因,我们通过选择不同类型的函子实现不同的功能。
此处我们通过 getter s
拿到了成员 a
,又用函数 f
将 a
转换为了函子 Functor a
,又将 setter s
作用到了函子上得到了新的 s
。对于 view
函数来说,我们只希望前半部分生效,也就是说我们希望 setter s <$> f a
仍返回 f a
,并且内部的值不变。
那什么样的函子能满足这个条件呢?这里我们可以定义一个 Const
函子类型,它的性质为任何函数作用在它上面都不会影响内部的值
newtype Const a b = Const { runConst :: a }
Const a
是函子类型类的实例
:{ instance Functor (Const a) where fmap _ (Const a) = Const a :}
我们可以尝试定义一个 Const
函子的实例,并且内部保存数字 1。我们在上面作用函数 (+10)
,通过 runConst
函数获取内部值可以发现保存的值仍为 1
c = Const 1 runConst $ (+10) <$> c
1
那么我们就可以实现 view
函数了
:{ view :: Lens s a -> s -> a view l s = runConst $ l Const s :}
为了使用方便可以将 view
实现为运算符 ^.
:{ infixr 4 ^. (^.) :: s -> Lens s a -> a (^.) s l = runConst $ l Const s :}
快来试一下吧
name ^. firstL
Cycoe
4. set
函数
set
函数用于设置聚合数据中的成员,接收透镜 l
、一个原始的聚合数据 s
和要设置的成员值 a
,返回新的聚合数据对象
set :: Lens s a -> a -> s -> s set l a s = ???
参考我们实现 view
的思路,在此处我们也需要选取一个合适的函子来完成 set
函数。但是与 view
函数中使用的 Const
函子不同,此处我们需要一个能把 setter s
函数作用到内部类型上的函子。标准库中其实已经内置了这个函子,就是 Identity
:{ newtype Identity a = Identity { runIdentity :: a } instance Functor Identity where fmap f (Identity a) = Identity $ f a :}
那么我们仿照 view
函数的方式补全 set
函数的实现
set :: Lens s a -> a -> s -> s set l a s = runIdentity $ l Identity s
这个实现对嗎?仔细观察一下就会发现问题,因为我们根本没有使用到变量 a
。再来分析一下 Lens
类型,此处我们希望的流程是通过 getter s
拿到原本的成员 a
,通过某一个函数将 a
转换为 Functor a
,最后再将 setter s
作用上去,并且我们希望忽略掉原本通过 getter
取出的成员 a
这里我们需要引入一个函数 const
,这个函数与 Const
函子不同
:{ const :: a -> b -> a const a _ = a :}
也就是说 const
函数可以绑定一个 a
类型的变量返回一个函数,这个函数不管输入什么都会返回原本绑定的变量 a
constF = const 1 constF 2 constF "Cycoe"
1 1
那么我们可以利用这个函数和 Identity
组合出一个新的函数 Identity . const a
,这个函数不管接收什么参数都会返回我们绑定的变量 a
,那么我们的 set
函数可以实现为
:{ set :: Lens s a -> a -> s -> s set l a s = runIdentity $ l (Identity . const a) s :}
同样的定义一个对应的运算符 .~
:{ infixr 4 .~ (.~) :: Lens s a -> a -> s -> s (.~) = set :}
使用 set
函数设置成员
firstL .~ "Handsome" $ name
Name {_first = "Handsome", _last = "Joo"}
5. over
函数
over
函数的功能是通过一个变换函数 a -> a
修改聚合类型中的成员,有了 set
函数的经验我们可以非常简单地写出 over
函数的实现
:{ over :: Lens s a -> (a -> a) -> s -> s over l f s = runIdentity $ l (Identity . f) s :}
同样地,定义 over
函数对应的运算符
:{ infixr 4 %~ (%~) :: Lens s a -> (a -> a) -> s -> s (%~) = over :}
使用 over
函数将 first name 变为全部字母大写
import Data.Char (toUpper) firstL %~ (map toUpper) $ name
Name {_first = "CYCOE", _last = "Joo"}
6. 总结
有了 Lens
类型和 view
、 set
和 over
函数,我们可以方便地对聚合类型中的成员执行查看、修改与变换操作。下一篇 Blog 中我们将探讨如何处理泛型类型,即将形如 Data a
的类型变换为 Data b
,以及如何处理嵌套的聚合类型