18

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

Clojure 的assoc-in
Haskell 的lens

4

4 回答 4

23

Clojureassoc-in允许您使用整数和关键字指定通过嵌套数据结构的路径,并在该路径处引入新值。它有合作伙伴dissoc-in, get-in, 和update-in删除元素,在不删除的情况下获取它们,或者分别修改它们。

镜头是双向编程的一种特殊概念,您可以在其中指定两个数据源之间的链接,并且该链接允许您反映从一个数据源到另一个数据源的转换。在 Haskell 中,这意味着您可以构建透镜或类似透镜的值,将整个数据结构连接到它的某些部分,然后使用它们将更改从部分传输到整体。

这里有一个类比。如果我们看一下它的用法,assoc-in它是这样写的

(assoc-in whole path subpart)

path我们可能会通过将视为镜头和assoc-in镜头组合器来获得一些见解。以类似的方式编写(使用 Haskelllens包)

set lens subpart whole

以便我们assoc-insetpath连接lens。我们也可以完成表格

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

这是相似之处的开始,但也有巨大的不同。在许多方面,lens它比*-inClojure 函数家族更通用。通常这对 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

现在我们几乎可以直接使用setasassoc-in了。

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

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

这在某种程度上显然有更多的语法噪音,但它表示数据类型的“路径”意味着什么的更高程度的明确性,特别是它表示我们是下降到数组还是字典。如果我们愿意,我们可以通过Clj在 Haskell 类型类中实例化来消除一些额外的噪音Ixed,但在这一点上几乎不值得。

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

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

重要的是,这意味着当我们有一个真正的镜头时,我们可以确定数据类型下降不会失败——这种保证在 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 有一个更成熟的类型概念,而 Lenses 作为一流的对象,广泛概括了简单地用*-in函数呈现的获取和设置的概念。

于 2014-01-22T20:22:01.570 回答
9

你说的是两种截然不同的东西。

您可以使用 lens 来解决与 assoc-in 类似的问题,其中您使用的集合类型 ( Data.Map, Data.Vector) 匹配语义但存在差异。

在像 Clojure 这样的非类型化语言中,通常根据具有非静态内容(散列映射、向量等)的集合来构造域数据,即使它是对通常是静态的数据进行建模。

在 Haskell 中,您将使用记录和 ADT 来构建数据,虽然您可以表达可能存在或可能不存在的内容(或包装一个集合),但默认是静态已知的内容。

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

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

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

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

tl;dr lensLens和其他类似的库更通用、更有用、类型安全,并且在惰性/纯 FP 语言中特别好。

于 2014-01-22T19:46:37.543 回答
6

assoc-in可以比lens某些情况下更通用,因为如果它们不存在,它可以在结构中创建级别。

lens提供Folds,拆除结构并返回包含值的摘要,并Traversals修改结构中的元素(可能同时针对多个元素,如果目标元素不存在,则可能不执行任何操作)同时保持结构的整体“形状”。但我认为使用lens.

我在 Clojure 中看到的与assoc-in-like 函数的另一个区别是,它们似乎只关心获取和设置值,而镜头的定义支持“用值做某事”,这可能涉及副作用。

例如,假设我们有一个 tuple (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
于 2014-01-22T21:12:30.720 回答
2

这个问题有点类似于询问 Clojurefor和 Haskell 的单子之间有什么区别。到目前为止,我将模仿答案:肯定for有点像Listmonad,但 monad 更加通用和强大。

但是,这有点愚蠢,对吧?单子已在 Clojure 中实现。为什么不一直使用它们?Clojure 的核心是关于如何处理状态的不同哲学,但仍然可以在其库中随意借鉴 Haskell 等伟大语言的好想法。

所以,当然,assoc-in, get-in,update-in等有点像关联数据结构的镜头。Clojure 中一般都有镜头的实现。为什么不一直使用它们?这是哲学上的差异(也许是一种怪异的感觉,即使用所有的 setter 和 getter,我们将在 Clojure 中创建另一个 Java,并最终以某种方式嫁给我们的母亲)。但是,Clojure 可以随意借鉴好的想法,并且您可以看到受镜头启发的方法进入 Om 和 Enliven 等很酷的项目。

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

于 2014-01-23T02:40:15.513 回答