Haskell - 初识 Lens

发布于 2025-01-31 21:50:37 字数 8666 浏览 5 评论 0

0.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)的能力。

0.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 帮助我们将 settergetter 函数构造成 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 函数时,一种常用的思路是根据组合的方式拼出需要的类型,再来验证是否正确,此处我们也可以这样做。我们使用 getters 对象中获取出成员 a ,再使用函数 fa 转换为 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

0.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 的参数中我们已经有了 ls ,还缺少一个函数 f 将类型 a 转换为函子。此处函子类型的选择是关键,这也是 Lens 类型中引入函子类型的原因,我们通过选择不同类型的函子实现不同的功能。

此处我们通过 getter s 拿到了成员 a ,又用函数 fa 转换为了函子 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

0.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"}

0.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"}

0.6 总结

有了 Lens 类型和 viewsetover 函数,我们可以方便地对聚合类型中的成员执行查看、修改与变换操作。下一篇 Blog 中我们将探讨如何处理泛型类型,即将形如 Data a 的类型变换为 Data b ,以及如何处理嵌套的聚合类型

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

韶华倾负

暂无简介

文章
评论
27 人气
更多

推荐作者

冰魂雪魄

文章 0 评论 0

qq_Wl4Sbi

文章 0 评论 0

柳家齐

文章 0 评论 0

无法言说的痛

文章 0 评论 0

魄砕の薆

文章 0 评论 0

盗琴音

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文