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。我們可以想像到那個代表GemLevelInt在函數間傳遞的效果。

同理,如果我們只想得到武器上的寶石的話,我們需要的是這樣的東西:

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

現在,我們已經分別實現了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 -> aview lens b = getConst $ lens Const b

到這裡為止,我們便實現了文章開頭所演示的 Lens 的功能。

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


推薦閱讀:

TAG:Haskell | 函数式编程 | Lens |