在操纵不可变数据结构时,Clojure assoc-in和Haskell镜头之间有什么区别?

时间:2014-01-22 19:05:42

标签: haskell clojure functional-programming immutability lens

我需要操作和修改深层嵌套的不可变集合(地图和列表),我想更好地理解不同的方法。这两个库解决了或多或少相同的问题,对吧?它们有何不同,哪种类型的问题更适合另一种?

Clojure's assoc-in
Haskell's lens

4 个答案:

答案 0 :(得分:23)

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 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类型类Clj中实例化Ixed来消除一些额外的噪音,但此时几乎不值得。

相反,要点是assoc-in适用于非常特殊的数据下降。由于Clojure的动态类型和IFn的重载,它比我上面列出的类型更通用,但是一个非常类似的固定结构可以嵌入Haskell而不需要进一步的努力。

镜片可以更进一步,并且具有更高的类型安全性。例如,上面的示例实际上不是真正的“镜头”,而是“Prism”或“Traversal”,它允许类型系统静态地识别未能进行遍历的可能性。它会迫使我们考虑这样的错误条件(即使我们选择忽略它们)。

重要的是,这意味着我们可以确定当我们有一个 true 镜头时,数据类型下降不会失败 - 在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函数表示的获取和设置的概念。

答案 1 :(得分:9)

你说的是两件不同的事情。

你可以使用镜头来解决与assoc-in类似的问题,你在使用与语义匹配的集合类型(Data.MapData.Vector),但是存在差异。

在像Clojure这样的无类型语言中,通常根据具有非静态内容(哈希映射,向量等)的集合来构建域数据,即使它是通常静态的建模数据。

在Haskell中,您将使用记录和ADT构建数据,当您可以表达可能存在或可能不存在的内容(或包装集合)时,默认值是静态已知的内容。 / p>

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

这些示例表明,当您的路径和类型与结构/数据不匹配时,它会启动Nothing而不是Just a

除了提供声音的getter / setter行为之外,镜头不会。它没有表达对数据外观的特殊期望,而assoc-in仅对具有可能非确定性内容的关联集合有意义。

这里的另一个区别是纯度和懒惰与严格和不纯的语义。在Haskell中,如果你从未使用过“旧”状态,只使用最新状态,那么只会实现该值。

tl;在Lens和其他类似库中找到的dr镜头更通用,更实用,类型安全,在懒惰/纯FP语言中特别好。

答案 2 :(得分:6)

在某些情况下,

assoc-in可能比lens更通用,因为如果它们不存在,它可以在结构中创建等级。

lens提供Folds,拆除结构并返回包含值的摘要,以及Traversals修改结构中的元素(可能同时定位多个元素,可能如果目标元素不存在则不做任何事情,同时保持结构的整体“形状”。但我认为使用lens创建中间级别很难。

我在Clojure中看到的与assoc-in类似的函数的另一个不同之处在于,它们似乎只关注获取和设置值,而镜头的定义支持“用价值做某事”,可能涉及副作用的东西。

例如,假设我们有一个元组(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

答案 3 :(得分:2)

这个问题有点类似于询问Clojure的for和Haskell的monad之间的区别。到目前为止,我将模仿答案:确保for有点像List monad,但monad更加通用和强大。

但是,这有点傻,对吧? Monad已在Clojure中实施。他们为什么不一直使用? Clojure的核心是关于如何处理状态的不同哲学,但仍然可以在其库中借用像Haskell这样的优秀语言的好主意。

所以,当然,assoc-inget-inupdate-in等有点像关联数据结构的镜头。在Clojure中有一般的镜头实现。他们为什么不一直使用?这是哲学上的一个不同之处(也许是令人毛骨悚然的感觉,与所有的制定者和吸气者一起,我们将在Clojure中制作另一个Java,并以某种方式最终与我们的母亲结婚)。但是,Clojure可以自由地借用好的想法,你可以看到镜头启发的方法进入像Om和Enliven这样的酷项目。

你必须小心地提出这样的问题,因为像兄弟姐妹一样占据同一个空间的Clojure和Haskell必然会互相借用并争论谁是对的。