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 -> HerosetWeapon :: Weapon -> Hero -> Hero-- and so on.
對於簡單的從深層結構中提取出一個值仍然是可接受的:
gemLevel.magicGem.weapon $ hero-- Orhero & (weapon>>>magicGem>>>gemLevel)
但修改內層數據(並返回一個新的對象)則顯得過於繁雜:
hero = hero { weapon = (weapon hero) { magicGem = (magicGem.weapon $ hero){ gemName = "WTF" }}}
可以看到,這裡僅僅三層嵌套,一個修改的操作就已經及其複雜了。
為了解決這個問題 Haskell 語言中有一種被稱為「Lens」的工具,可是實現下面這樣的寫法:
view (weaponLens.magicGemLens.gemLevelLens) herohero = set (weaponLens.magicGemLens.gemNameLens) "Gem" herohero = 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 -> bviewL (g, _) = gsetL :: L a b -> b -> a -> asetL (_, s) = soverL :: 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) herohero = setL (weaponL.>magicGemL.>gemLevelL) 2 hero
我們的實現仍然藉助了複合的思想,但是需要我們自己來實現針對 getter 和 setter 的複合,而前文演示的卻是真正的函數複合。
實現 Lens 的準備工作
我們已經注意到文章開頭的 Lens 有幾個特點
- 是普通的函數類型,可以互相複合;
- 與對象類型
b
和域類型a
相關; - 可以用來實現看似相反的兩個操作 get 和 set。
下面我們嘗試找出這個類型
type Lens b a = (??? ) -> (???)
考慮它的複合特點,按照結構的嵌套順序,從前向後依次是從內向外:
aL :: Lens b acL :: Lens a caL.cL :: Lens b c
要實現這樣的複合特性,應當是
type SomeType a = ...aL :: SomeType a -> SomeType bcL :: SomeType c -> SoemType aaL.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 -> aview 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 WeaponweaponVLens f = h -> f (weapon h)magicGemVLens :: VLens Weapon GemmagicGemVLens f = w -> f (magicGem w)gemLevelVLens :: VLens Gem IntgemLevelVLens 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 WeaponweaponOLens f = h -> (`setWeapon` h) $ f (weapon h)magicGemOLens :: OLens Weapon GemmagicGemOLens f = w -> (`setMagicGem` w) $ f (magicGem w)gemLevelOLens :: OLens Gem IntgemLevelOLens 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 -> aview lens b = getConst $ lens Const b
到這裡為止,我們便實現了文章開頭所演示的 Lens 的功能。
附上在Zju Lambda報告的Slides:The Overview of Lens
推薦閱讀: