Lens:从入门到入门

Lens:从入门到入门

孙浩然孙浩然

概览

Haskell 语言中操作一个复杂的数据结构往往会成为一个问题。

例如我们用 Haskell 做一个RPG游戏,有下面的定义:

data Hero = Hero {
  heroLevel :: Int, weapon :: Weapon
}

data Weapon = Weapon {
  basicAttack :: Int, weaponLevel :: Int, magicGem :: Gem
}
data Gem = Gem {
  gemLevel :: Int,
  gemName :: String
} 
setHeroLevel :: Hero -> Int -> Hero
setWeapon    :: Weapon -> Hero -> Hero
-- and so on. 

对于简单的从深层结构中提取出一个值仍然是可接受的:

gemLevel.magicGem.weapon $ hero
-- Or
hero & (weapon>>>magicGem>>>gemLevel)

但修改内层数据(并返回一个新的对象)则显得过于繁杂:

hero' = hero {
    weapon = (weapon hero) {
        magicGem = (magicGem.weapon $ hero){
            gemName = "WTF" }}}

可以看到,这里仅仅三层嵌套,一个修改的操作就已经及其复杂了。

为了解决这个问题 Haskell 语言中有一种被称为「Lens」的工具,可是实现下面这样的写法:

view (weaponLens.magicGemLens.gemLevelLens) hero
hero'  = set (weaponLens.magicGemLens.gemNameLens) "Gem" hero
hero'' = over (weaponLens.magicGemLens.gemLevelLens) (+1) hero

-- 中缀版本
hero .^ weaponLens.magicGemLens.gemLevelLens 
hero'  = hero & weaponLens.magicGemLens.gemNameLens .~ "Gem" 
hero'' = hero & weaponLens.magicGemLens.gemLevelLens) %~ (+1)

这里的代码已经非常接近于普通的命令式语言中的写法了,非常自然、易用。

普通的命令式语言中用 . 从一个结构中提取它的一个子域,而这里我们在 Haskell 中通过 Lens 实现了类似的效果。同时我们注意到,这里的. 不是凭空出现的,而是我们熟悉的 Haskell 中的函数复合。即 Lens 完成上面这些复杂操作的一个基本思路是复合。

简版 Lens

实际上,如果我们已经有了这些对象对应的 getter 和 setter 函数,那么我们不难将他们之间互相复合形成操作深层数据的新的 getter 和 setter。

type L a b = (a -> b, b -> a -> a)

(.>) :: L a b -> L b c -> L a c
(g1, s1) .> (g2, s2) = (g2 . g1, \c a -> s1 (s2 c (g1 a)) a)

viewL :: L a b -> a -> b
viewL (g, _) = g

setL :: L a b -> b -> a -> a
setL (_, s) = s

overL :: L a b -> (b -> b) -> a -> a 
overL (g, s) f a = s (f $ g a) a

我们直接将一个 getter 与 setter 包装成二元组。

weaponL   = (weapon, setWeapon)
gemLevelL = (gemLevel, setGemLevel) 

这种情况下,我们定义的“简版 Lens ”的使用与前文演示的 Lens 是极其相似的:

viewL (weaponL.>magicGemL.>gemLevelL) hero
hero' = setL (weaponL.>magicGemL.>gemLevelL) 2 hero

我们的实现仍然借助了复合的思想,但是需要我们自己来实现针对 getter 和 setter 的复合,而前文演示的却是真正的函数复合。


实现 Lens 的准备工作

我们已经注意到文章开头的 Lens 有几个特点

  • 是普通的函数类型,可以互相复合;
  • 与对象类型b 和域类型 a 相关;
  • 可以用来实现看似相反的两个操作 get 和 set。

下面我们尝试找出这个类型

type Lens b a = (???  ) -> (???)

考虑它的复合特点,按照结构的嵌套顺序,从前向后依次是从内向外:

aL :: Lens b a
cL :: Lens a c
aL.cL :: Lens b c

要实现这样的复合特性,应当是

type SomeType a = ...

aL :: SomeType a -> SomeType b
cL :: SomeType c -> SoemType a
aL.cL :: SomeType c -> SomeType b

即上面的第一个 (???) 与a有关,第二个与b有关。

同时,Lens b a一定会接受一个b类型的参数作为要操作的主体对象,我们可以进一步写成

type Lens b a = (???) -> (b -> ???)

而上面的复合特性要求前后是两个类似的类型SomeType,我们进一步改写为

type Lens b a = (a -> ???) -> (b -> ???)

我们可以猜测到,view over 等函数调用Lens,传递进一个函数(a -> ???)来实现了不同的操作。

View Lens 的实现

我们先尝试写出一个特定类型的 Lens ,来只支持view操作,根据上文的分析,view函数的定义应该形如下:

view :: VLens b a -> b -> a
view lens b = lens ??? b

考虑我们之前的例子

weaponVLens   :: (Weapon -> ???) -> (Hero -> ???)
magicGemVLens :: (Gem -> ???)    -> (Weapon -> ???)
gemLevelVLens :: (Int -> ???)    -> (Gem -> ???)

如果我们想要获得英雄的武器上的宝石的宝石等级,那么我们想要的可能是这样的东西:

weaponVLens.magicGemVLens.gemLevelVLens :: (Int -> Int) -> (Hero -> Int)

这样,view函数便可以对这个复合的VLens传入某个函数,再传入我们的英雄,就可以得到宝石等级了。为了让这样的复合成为可能,上面的所有 ??? 都必须是Int。我们可以想象到那个代表GemLevelInt在函数间传递的效果。

同理,如果我们只想得到武器上的宝石的话,我们需要的是这样的东西:

weaponVLens.magicGemVLens :: (Gem -> Gem) -> (Hero -> Gem)

这时候这些???又成为了Gem

由此可见,在view的场合下,这里的类型???随着提取的东西不同而变化,并且等于我们要提取的东西的类型。这样VLens的类型定义便得到了:

type VLens b a = forall c. (a -> c) -> (b -> c)

所有的VLens在复合时都接受内层的一个提取操作,并返回一个嵌套了的提取操作。

weaponVLens  :: VLens Hero Weapon
weaponVLens f = \h -> f (weapon h)

magicGemVLens :: VLens Weapon Gem
magicGemVLens f = \w -> f (magicGem w)

gemLevelVLens :: VLens Gem Int
gemLevelVLens f = \g -> f (gemLevel g)

而最终传入我们要操作的外层对象之后,则用相仿的顺序,一层层地完成了提取操作,直到最内层,这时我们只需要使用id函数使其原样返回即可。

由此,view函数的定义便可以得到了。


viewV vlens b = vlens id b

Over Lens 的实现

再次考虑我们的例子

weaponOLens   :: (Weapon -> ???) -> (Hero -> ???)
magicGemOLens :: (Gem -> ???)    -> (Weapon -> ???)
gemNameOLens  :: (Int -> ???)    -> (Gem -> ???)

现在我们希望对一个对象的某个域进行修改,并返回修改过了的对象。那么后面的 ??? 则应该与和它紧靠着的类型相同,而为了使这些OLens可以互相复合,前面的 ??? 应该与紧靠着的前面类型相同。例如:

weaponOLens.magicGemOLens :: (Gem -> Gem) -> (Hero -> Hero)

可以看到 OLens的类型比较简单

type OLens b a =  (a -> a) -> (b -> b)

观察一下便可以得到,第一个参数(a -> a)便是我们对域进行操作的更新函数了。此时over函数不需要再做其他多余的事情,只需要将OLens原样返回。而各个OLens的定义也只不过是产生一个新的修改函数,这个修改函数将自己管辖的域修改为已经被修改过了的内层对象。而最内层则会使用用户传入的修改函数f来完成相应的操作。

weaponOLens ::  OLens Hero Weapon
weaponOLens f = \h -> (`setWeapon` h) $ f (weapon h)

magicGemOLens :: OLens Weapon Gem
magicGemOLens f = \w -> (`setMagicGem` w) $ f (magicGem w)

gemLevelOLens :: OLens Gem Int
gemLevelOLens f = \g -> (`setGemLevel` g) $ f (gemLevel g)

这样,当最终传入需要处理的外层对象时,OLens便会一层层地完成修改的工作。


对于set而言,只不过是一种特殊的over

setO vlens s = vlens (const s)

最终实现 Lens

现在,我们已经分别实现了VLensOLens,而且发现他们之间有相似之处。实际上他们都是从一个相同的(a -> ???) -> (b -> ???) 经过我们一系列对其性质的分析得到的。我们的最终目的是通过单一的Lens类型,来实现overview这样不同的行为,即需要某种多态。而下面的类型却无法为这种多态提供帮助。

type Lens b a -> forall c d. (a -> c) -> (b -> d)

我们需要某种类型,我们可以对其中的内容进行操作,并且这种操作的行为随使用者的需求而可以多态变化。符合这样特点的,正是我们熟悉的 Functor 。所以我们便可以这样做了:

type Lens b a = Functor f => (a -> f a) -> (b -> f b)

这个类型定义非常类似前面的OLens,同时这里的f具有任意性,又可以满足VLens的需求。而实际上,如果这里的f是 Identity Functor 的话,这个类型所表达的与OLens毫无区别。我们只需要对原有over进行一层 Identity Functor 的包装就保证了其语义不变:

over lens f = runIdentity . lens (Identity . f)

对于VLens类型

weaponOLens ::  (a -> a) -> (b -> b)
weaponOLens f h = (`setWeapon` h) $ f (weapon h)

我们也仅需要对调用的修改本层次域的函数进行升格,上下结构保持了极好的相似性。

weaponLens ::  Functor f => (a -> f a) -> (b -> f b)
weaponLens f h = (`setWeapon` h) <$> f (weapon h)

那么我们剩余的问题就在于如何利用 Functor 提供的多态能力来实现view的语义了。

我们观察一下先前的view实现

type VLens b a = forall c. (a -> c) -> (b -> c)
viewV vlens b = vlens id b

我们发现view的实现有如下特点:

  • 对象的内容完全没有被改变
  • 每一层的作用是返回内层的内容

我们只要找到一个Functor符合上面的特点就可以实现view,而 Const Functor 恰好符合我们的需求。

newtype Const a b = Const {getConst :: a}
instance Functor (Const a) where
  fmap f c = c

Const Functor 的fmap并不改变值,实际上,其中根本没有值。一个 Const Functor 在创建之后,经历过多次fmap可能其类型发生变化,但getConst所取出的内容永远不会变化。

考虑下面的view定义,最内层的值被应用到Const构建函数上。之后经历过若干次fmap,最后getConst取得的仍是原来的值,于是便实现了view的行为。

view :: Lens b a -> b -> a
view lens b = getConst $ lens Const b

到这里为止,我们便实现了文章开头所演示的 Lens 的功能。

附上在Zju Lambda报告的Slides:The Overview of Lens

8 条评论
推荐阅读