簡體   English   中英

在操縱不可變數據結構時,Clojure的assoc-in和Haskell鏡頭之間有什么區別?

[英]When manipulating immutable datastructures, what's the difference between Clojure's assoc-in and Haskell's lenses?

我需要操作和修改深層嵌套的不可變集合(映射和列表),我想更好地理解不同的方法。 這兩個庫解決了或多或少相同的問題,對吧? 它們有何不同,哪種類型的問題更適合另一種?

Clojure的合作assoc-in
哈斯克爾的lens

Clojure的assoc-in允許您使用整數和關鍵字指定嵌套數據結構的路徑,並在該路徑中引入新值。 它有合作伙伴dissoc-inget-inupdate-in ,分別刪除元素,不刪除它們或修改它們。

鏡頭是雙向編程的一個特定概念,您可以在其中指定兩個數據源之間的鏈接,並且該鏈接允許您反映從一個到另一個的轉換。 在Haskell中,這意味着您可以構建鏡頭或類似鏡頭的值,將整個數據結構連接到其某些部分,然后使用它們將部件之間的變化傳遞給整體。

這里有個比喻。 如果我們看一下assoc-in的用法就像是

(assoc-in whole path subpart)

我們可能會受到的思維獲得一些有識之士path鏡頭和assoc-in的鏡頭組合子。 以類似的方式你可能寫(使用Haskell lens包)

set lens subpart whole

讓我們連線assoc-insetpathlens 我們也可以填寫表格

set          assoc-in
view         get-in
over         update-in
(unneeded)   dissoc-in       -- this is special because `at` and `over`
                             -- strictly generalize dissoc-in

這是相似之處的開始,但也有很大的不同。 在許多方面, lens比Clojure函數的*-in族更通用。 通常,這對Clojure來說不是問題,因為大多數Clojure數據存儲在由列表和字典組成的嵌套結構中。 Haskell非常自由地使用更多自定義類型,其類型系統反映了有關它們的信息。 鏡頭概括了*-in系列函數,因為它們可以在更復雜的域上順利運行。

首先,讓我們在Haskell中嵌入Clojure類型並編寫*-in函數族。

type Dict a = Map String a

data Clj 
  = CljVal             -- Dynamically typed Clojure value, 
                       -- not an array or dictionary
  | CljAry  [Clj]      -- Array of Clojure types
  | CljDict (Dict Clj) -- Dictionary of Clojure types

makePrisms ''Clj

現在我們幾乎可以直接使用set as assoc-in

(assoc-in whole [1 :foo :bar 3] part)

set ( _CljAry  . ix 1 
    . _CljDict . ix "foo" 
    . _CljDict . ix "bar" 
    . _CljAry  . ix 3
    ) part whole

這顯然有更多的語法噪音,但它表示對數據類型中的“路徑”意味着更高程度的顯式性,特別是它表示我們是否正在下降到數組或字典中。 如果我們想要的話,我們可以通過在Haskell類型類Ixed實例化Clj來消除一些額外的噪聲,但是在這一點上它幾乎不值得。

相反,要點是, assoc-in適用於非常特殊的數據下降。 由於Clojure的動態類型和IFn重載,它比我上面列出的類型更通用,但是一個非常類似的固定結構可以嵌入Haskell IFn需要進一步的努力。

透鏡可以更進一步,並且具有更高的類型安全性。 例如,上面的示例實際上不是真正的“鏡頭”,而是“Prism”或“Traversal”,它允許類型系統靜態地識別未能進行遍歷的可能性。 它會迫使我們考慮這樣的錯誤條件(即使我們選擇忽略它們)。

重要的是,這意味着我們可以確定當我們擁有一個真正的鏡頭時,數據類型下降不會失敗 - 在Clojure中無法做出這種保證。

我們可以定義自定義數據類型並制作以類型安全的方式下載到它們中的自定義鏡頭。

data Point = 
  Point { _latitude  :: Double
        , _longitude :: Double
        , _meta      :: Map String String }
  deriving Show

makeLenses ''Point

> let p0 = Point 0 0
> let p1 = set latitude 3 p0
> view latitude p1
3.0
> view longitude p1
0.0
> let p2 = set (meta . ix "foo") "bar" p1
> preview (meta . ix "bar") p2
Nothing
> preview (meta . ix "foo") p2 
Just "bar"

我們還可以推廣到同時針對多個相似子部分的鏡頭(真正的遍歷)

dimensions :: Lens Point Double

> let p3 = over dimensions (+ 10) p0
> get latitude p3
10.0
> get longitude p3
10.0
> toListOf dimensions p3
[10.0, 10.0]

甚至可以定位模擬的子部件,這些子部件實際上並不存在,但仍然形成我們數據的等效描述

eulerAnglePhi   :: Lens Point Double
eulerAngleTheta :: Lens Point Double
eulerAnglePsi   :: Lens Point Double

從廣義上講,Lenses概括了Clojure *-in函數族抽象的整個值和值的子部分之間基於路徑的相互作用。 你可以在Haskell中做更多的事情,因為Haskell有一個更加發達的類型和鏡頭概念,作為第一類對象,廣泛地概括了獲取和設置的概念,簡單地用*-in函數表示。

你說的是兩件不同的事情。

您可以使用鏡頭來解決與assoc-in類似的問題,您可以使用與語義匹配的集合類型( Data.MapData.Vector ),但存在差異。

在像Clojure這樣的無類型語言中,通常根據具有非靜態內容(哈希映射,向量等)的集合構建域數據,即使它是通常靜態的建模數據。

在Haskell中,您將使用記錄和ADT構建數據,在這里您可以表達可能存在或可能不存在的內容(或包裝集合),但默認值是靜態已知內容。

要查看的一個庫是http://hackage.haskell.org/package/lens-aeson ,其中包含可能具有不同內容的JSON文檔。

這些示例表明,當您的路徑和類型與結構/數據不匹配時,它會踢出Nothing而不是Just a

除了提供聲音吸氣/設定器行為之外,鏡頭不會任何事情。 它沒有表達對數據外觀的特定期望,而assoc-in僅對具有可能非確定性內容的關聯集合有意義。

這里的另一個區別是純度和懶惰與嚴格和不純的語義。 在Haskell中,如果你從未使用過“舊”狀態,只使用最新狀態,那么只會實現該值。

Lens和其他類似庫中的Lens可以更通用,更有用,類型安全,特別適用於惰性/純FP語言。

在某些情況下, assoc-in可以比lens更通用,因為如果它們不存在,它可以在結構中創建水平。

lens提供Folds ,拆除結構並返回包含值的摘要,以及修改結構中元素的Traversals (可能同時針對多個元素,如果目標元素不存在則可能無效),同時保持結構的整體“形狀”。 但我認為使用lens創建中間水平會很困難。

我跟看到另一個不同之處assoc-in用Clojure樣的功能是,這些似乎只關注獲取和設置值,而鏡頭本身的定義支持“做與價值的東西”,這東西可能涉及副作用效果。

例如,假設我們有一個元組(1,Right "ab") 第二個組件是可以包含字符串的sum類型。 我們想通過從控制台讀取字符串來更改字符串的第一個字符。 這可以通過鏡頭完成如下:

(_2._Right._Cons._1) (\_ -> getChar) (1,Right "ab")
-- reads char from console and returns the updated structure

如果字符串不存在或為空,則不執行任何操作:

(_2._Right._Cons._1) (\_ -> getChar) (1,Left 5)
-- nothing read

(_2._Right._Cons._1) (\_ -> getChar) (1,Right "")
-- nothing read

這個問題有點類似於詢問Clojure's for和Haskell's monad之間的區別。 我會模仿到目前為止的答案:肯定for是有點像一個List單子,但單子有這么多的通用和強大。

但是,這有點傻,對吧? Monad已在Clojure中實施。 他們為什么不一直使用? Clojure的核心是關於如何處理狀態的不同哲學,但仍然可以在其庫中借用像Haskell這樣的優秀語言的好主意。

因此,確定, assoc-inget-inupdate-in等有點像關聯數據結構的鏡頭。 在Clojure中有一般的鏡頭實現。 他們為什么不一直使用? 這是哲學上的一個不同之處(也許是令人毛骨悚然的感覺,與所有的制定者和吸氣者一起,我們將在Clojure中制作另一個Java,並以某種方式最終與我們的母親結婚)。 但是,Clojure可以隨意借用好的想法,你可以看到鏡頭啟發的方法進入像Om和Enliven這樣的酷項目。

你必須要小心地提出這樣的問題,因為像兄弟姐妹一樣占據同一個空間Clojure和Haskell必然會互相借用並爭論誰是對的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM