Lens: 從入門到再次入門

類型補全計畫

從上一篇我們可以看出,Lens 就是整合在一起的 GetterSetter,藉助set, over, view這三個函數,我們可以分別使用 Lens 的GetterSetter。但是我們目前的 Lens 類型定義並不是完整的,因此我們首先對 Lens 的類型進行補全。

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

首先讓我們看這樣一個例子:

set _1 ((1,2),3) 3set _1 ((1,2),3) True

在我們目前的類型定義上面的代碼第一行可以正常工作,而第二行則不可以。但是第二行確實是合乎邏輯的,我們的確有時候需要講一個原本是數字的地方設置為布爾值或是其他的什麼東西。

讓我們將原先的 Lens 類型定義進行簡單的改變,這樣就可以在通過 Lens 對數據操作時改變數據的類型。

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

我們可以可以直觀地解讀這個新的類型定義的含義,對s類型的量的一個a類型的域進行某種操作,之後該域變為b,相應的s變為t。當然這裡的st的關係並不是隨意的,而是依賴於ab的關係。

現在我們之前使用的 Lens 類型將定義如下:

type Lens" s a = Lens s s a a-- Or{-# LANGUAGE LiberalTypeSynonyms #-}type Simple f a b = f a a b btype Lens" = Simple Lens

在這個定義下我們可以定義出由gettersetter構建 Lens 的函數

lens :: (s -> a) -> (s -> b -> t) -> Lens s t a blens getter setter f s = setter s <$> f (getter s)

下面讓我們來回憶一下overview的定義

over lens f = runIdentity . lens (Identity . f)view lens b = getConst $ lens Const b

我們發現在f分別取IdentityConst的時候,Lens 就分別表現出了 SetterGetter的特性。對於viewover而言,他們只需要使用單一的 Functor ,因此我們可以專門定義GetterSetter的類型。

type Getting r s a = (a -> Const r a) -> s -> Const r stype Setter s t a b = (a -> Identity b) -> s -> Identity ttype Setting = Simple Setter

在有了這個定義之後,我們就可以修改之前三個常用函數的類型簽名:

view :: Getting a s a -> s -> aover :: Setter s t a b -> (a -> b) -> s -> tset :: Setter s t a b -> b -> s -> t

這樣的改變看似沒有什麼作用,但通過這樣的改變,三個常用函數不再以 Lens 為作用對象,而是更加聚焦於一類更加通用的類型。

作為概念積類型的類型與作為概念的值

通過之前的例子,我們可以看到我們用 Lens 來操作一個積類型(Product Type),例如元組、Record ;於此相對,Lens 不能用來操作和類型(Sum Type)。我們可以用 Lens 改變或是讀取積類型的某個部分(Component)的值。但是實際上,我們不需要一個實在的積類型,也不需要一個實在的部分,只需要概念上的積類型與概念上的部分即可。這樣的表述顯得非常抽象,讓我們來看幾個例子。

第一個例子是列表,我們將要操作的不是列表的元素這些實在的部分,而是抽象的部分,列表的長度

讓我們定義這個玄乎的_lengthLens :

_length :: Lens" [a] Int_length f l = const l <$> f (length l)

可以看出,它可以從一個列表中提取出它的長度,但是不會改變它的長度

view (_1._length) ("hello", 3)--> 5set (_1._length) 9 ("world", 3) --> ("world",3)

在這個例子中,長度並不是列表的一個實在的部分,我們操作的積類型也不是一個實在的積類型,而是概念中的某種包含長度的積類型。

再看第二個例子,我們操作一個數字的絕對值。

_abs :: (Num a, Ord a) => Lens" a a_abs f i = setabs <$> f (abs i) where sgn x | x >= 0 = 1 | x < 0 = -1 setabs x | x >= 0 = x*sgn i | x < 0 = error "Abs must be non-negative"view _abs -123--> 123set _abs 13 -99--> -13

可以看出,概念上,數字確實含有「絕對值」這一部分的值,但是數字與絕對值的關係同樣也不是「元組與每個元素」之間的關係,也不是「記錄與它的域」的關係。同時,數字本身,並不明顯是那種積類型,這裡我們同樣是將其看作了概念上的積類型。

引入這兩個例子的目的是說明,Lens 是某種更加抽象與普遍化的工具,它不僅僅用來處理具體的數據結構與數據結構內部的值,也可以用來處理各種各樣的情況;Lens 聚焦於某個數據結構(實在的或是概念上的)的某個值上,這無關乎這個值是實在地存在於這個數據結構里,還是抽象地、概念上地存在於這個數據上,這為我們以後利用 Lens 完成語義的表達提供了可行性。

多焦點數據操作

Lens 在工作的過程中,對某個數據結構內某個值應用了一個a -> f b的函數,並最終得到一個f t類型的新數據結構。假設現在我們想要操作某個列表[a]中的所有元素,那麼我們期望對列表中的每個元素應用a -> f b的函數,並且最終得到一個f [b]

對於應用某個函數於列表中的每個元素這一任務,我們有非常熟悉的解決方案map

map (f::a->f b) (xs::[a]) :: [f b]

但是我們期望得到的類型是f [b]而不是[f b],我們需要一個將函子的列錶轉換為列表的函子的函數,而實際上,函子沒有足夠的約束來支持這樣的操作,最簡單的例子就是,一個空函子的列表我們沒法直接找到對應的空列表的函子;此外更一般的場合我們需要將f a合併至f [a]得到一個新的f [a]的函數,其類型為f a -> f [a] -> f [a],而我們有的列表拼接函數的類型為a -> [a] -> [a],這也不是函子的升格可以直接完成的。而應用函子恰巧有我們需要的pure函數處理第一種情況,又有可以對雙參數函數升格的liftA2。由此看來,我們需要的是使用應用函子代替函子完成我們的需要。

有了這些分析,我們不難寫出將函子的列表提取為列表的函子的函數。

(<:>) :: Applicative f => f a -> f [a] -> f [a],(<:>) = liftA2 (:)sequenceA :: (Applicative f) => [f a] -> f [a] sequenceA [] = pure [] sequenceA (x:xs) = x <:> sequenceA xs

接下來我們需要的操作列表中全部元素的 Lens 的實現也可以容易給出,因為我們先前已經通過map實現了(a -> f b) -> [a] -> [f b],只需再對結果應用剛實現的sequenceA就可以恰好得到我們需要的 Lens 類型。

_every :: Applicative f => (a -> f b) -> [a] -> f [b]_every f xs = sequenceA $ map f xsover _every (+1) [2,3,4]--> [3,4,5]

由於應用函子是特殊的函子,所以_every是特殊的 Lens, 我們將這類 Lens 命名為 Traversal。

type Traversal s t a b = Applicative f => (a -> f b) -> s -> f ttype Traversal" s a = Traversal s s a a

取這個名字的原因是,在標準庫Data.Traversal中實際上恰好有一個函數traverse符合我們的要求, 這個函數並非為 Lens 專門設計,但它的類型恰好與我們先前的_every 相同,不僅如此這個函數不僅可以在列表上工作,也可以在所有Traversable類型上工作。由於這類 Lens 操作均依賴於traverse, 所以取名為 Traversal 。

讓我們簡單地看一下 Traversal 是如何與over一通工作的:

traverse :: Traversable t => Traversal (t a) (t b) a bover traverse (+1) [2,3,4]runIdentity . traverse (Identity . (+1)) [2,3,4]runIdentity $ sequenceA $ map (Identity . (+1)) [2,3,4]runIdentity $ sequenceA [Identity 3, Identity 4, Identity 5]runIdentity $ Identity 3 <:> Identity 4 <:> Identity 5 <:> Identity []runIdentity $ Identity [3, 4, 5]--> [3,4,5]

但是,現在我們的 Traversal 不能正確與view工作。 例如,對於下面的代碼

view traverse ["1","2","3"]view traverse ["1","2","3"]

我們期望的結果是這樣的,我們只需要它原封不動地返回即可

["1","2","3"][1,2,3]

而實際上,我們得到了看似匪夷所思的結果

"123"-- error: No instance for (Monoid Char) arising from a use of 『traverse』

讓我們展開 Traversal 與view工作的過程

view traverse ["1","2","3"]getConst $ traverse (Const b) ["1","2","3"]getConst $ sequenceA $ map (Const b) ["1","2","3"]getConst $ sequenceA [Const "1", Const "2", Const "3"]getConst $ sequenceA $ Const "1" <:> Const "2" <:> Const "3" <:> pure []

我們實際上清楚Const a本身並非應用函子,Monoid a => Const a才是 ,這個a類型的值不是「容器」內的值,而是「容器」的一部分,容器內並不存在值。於是,將應用函子範疇上的值應用到應用函子範疇上的函數時,內部並無操作,有的只是「容器」的合併。

instance Monoid a => Applicative (Const a) where pure _ = Const empty (Const x) <*> (Const y) = Const (x <> y)

列表的默認mappend操作是列表合併,所以我們就可以將上面的計算繼續寫下去了

getConst $ sequenceA Const "1" <:> Const "2" <:> Const "3" <:> pure []getConst $ sequenceA Const "1" <:> Const "2" <:> Const "3" <:> Const ""getConst $ sequenceA Const "1" <:> Const "2" <:> Const "3"getConst $ sequenceA Const "1" <:> Const "23"getConst $ sequenceA Const "123""123"

可以看出,這並不是匪夷所思的結果,而是在這些定義下的合理結果。實際上,從view類型上我們也可以看出它的確做了它應當的工作。

view :: Getting a s a -> s -> aview traverse ["1","2","3"]-- view :: Getting String [String] String -> [String] -> String

它最終確實給了我們一個字元串。

所以,我們需要構建專用於 Traversal 的view函數。考慮之前view的定義

view :: Getting a b a -> b -> aview lens = getConst . lens Const

這裡他將原來的值不加改變地餵給了getConst, 我們可以講原始值套在一個 Monoid 里完成我們需要的效果。這裡,我們需要的就是最終得到一個列表,因此只需要再套一層列表的 Monoid 即可,外面套的這層 Monoid 會互相合併,最終只剩下一個列表,裡面排滿了原來的元素。

toListOf :: Getting [a] s a -> s -> [a]toListOf lens = getConst . lens (x -> Const [x])toListOf traverse ["1","2","3"]--> ["1","2","3"]toListOf traverse ["1","2","3"]--> ["1","2","3"]

這個函數似乎沒有多大用處,它原封不動地返回了原本的列表。但是,我們可以依託於traverse 構建更多更有用的 Traversal。

例如聚焦於一個列表中全部滿足某個條件的 Traversal

_all :: (a -> Bool) -> Traversal" [a] a_all st f s = traverse update s where update old = if st old then f old else pure old toListOf (_all (/=0)) [1,2,0,3,4,0,5]--> [1,2,3,4,5]

同時,我們描述過 Traversal 是一種特殊的 Lens, 所以它具有 Lens 各種有用的性質,例如通過互相複合來處理嵌套的列表。

toListOf (traverse.traverse) [[1,2],[1,2,3,4]]--> [1,2,1,2,3,4]xs = [[1,2],[1,2,3,4],[4,5,6],[23,4,5,5,4],[1],[2,3]]over (_all (x-> length x <= 3 ) .traverse) (+1) xs-- >[[2,3],[1,2,3,4],[5,6,7],[23,4,5,5,4],[2],[3,4]]

或者與普通的 Lens 複合,來完成複雜的操作。

toListOf (traverse._1) [(1,2),(3,4),(5,6)]--> [1,3,5]

需要注意的是,Traversal 是特殊的 Lens,也就是說在 Lens 上多出一些特定的限制,因此 Traversal 與普通 Lens 的複合將會仍然繼承這些限制,即 Traversal 與 Lens 的複合仍是 Traversal。

使用更多 Monoid 來獲得多種效果

在上面我們使用了[]這一 Monoid 來實現了合成列表的效果,實際上,我們還可以使用其他Monoid。下面讓我們看幾個例子。

第一個例子是將包裝過的 Maybe作為一個 Monoid,並取名為 First 。從名字我們也可以看出來,它的作用就是取出列表的首個元素。

newtype First a = First (Maybe a)instance Monoid (First a) where mempty = First Nothing mappend (First Nothing) y = y mappend x _ = xpreview :: Getting (First a) s a -> s -> Maybe apreview lens = getFirst . getConst . lens (Const . First . Just)preview (_all (/=0)) [3, 2, 1, 0]--> Just 3preview (_all (/=0)) [0,0,0]--> Nothing

同樣,修改 mappend的定義我們可以得到 Last,這裡不再具體給出。

第二個例子是包裝過的Bool,我們可以用它來判斷一個列表中是否含有某個元素

newtype Any = Any { getAny :: Bool }instance Monoid Any where mempty = Any False Any x `mappend` Any y = Any (x || y) has :: Getting Any s a -> s -> Boolhas l = getAny . getConst . l (const $ Const (Any True)) has (_all (==0)) [3, 2, 1, 0]--> True

我們發現,由於 Traversal 具有良好的抽象能力,我們僅僅選用不同的 Monoid 就實現了多種多用的效果,這無疑是非常令人振奮的。

-- 2017/11/25日更新

出現了各種錯誤(捂臉),謝謝 @腳本少女魔理沙 指正╮(╯▽╰)╭


推薦閱讀:

React 0.14:揭秘局部組件狀態陷阱
Python進階:函數式編程實例(附代碼)
國內有沒有學校講Lisp或者函數式編程呢?

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