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 ,以及如何处理嵌套的聚合类型
本站文章如未特殊声明,默认采用