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
。我们可以想象到那个代表GemLevel
的Int
在函数间传递的效果。
同理,如果我们只想得到武器上的宝石的话,我们需要的是这样的东西:
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
现在,我们已经分别实现了VLens
和OLens
,而且发现他们之间有相似之处。实际上他们都是从一个相同的(a -> ???) -> (b -> ???)
经过我们一系列对其性质的分析得到的。我们的最终目的是通过单一的Lens
类型,来实现over
和view
这样不同的行为,即需要某种多态。而下面的类型却无法为这种多态提供帮助。
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