我需要操作和修改深度嵌套的不可变集合(地图和列表),并且我想更好地理解不同的方法。这两个库或多或少解决了相同的问题,对吧?它们有什么不同,一种方法比另一种方法更适合哪种类型的问题?
4 回答
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-in
与set
和path
连接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
它比*-in
Clojure 函数家族更通用。通常这对 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
asassoc-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
函数呈现的获取和设置的概念。
你说的是两种截然不同的东西。
您可以使用 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 语言中特别好。
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
这个问题有点类似于询问 Clojurefor
和 Haskell 的单子之间有什么区别。到目前为止,我将模仿答案:肯定for
有点像List
monad,但 monad 更加通用和强大。
但是,这有点愚蠢,对吧?单子已在 Clojure 中实现。为什么不一直使用它们?Clojure 的核心是关于如何处理状态的不同哲学,但仍然可以在其库中随意借鉴 Haskell 等伟大语言的好想法。
所以,当然,assoc-in
, get-in
,update-in
等有点像关联数据结构的镜头。Clojure 中一般都有镜头的实现。为什么不一直使用它们?这是哲学上的差异(也许是一种怪异的感觉,即使用所有的 setter 和 getter,我们将在 Clojure 中创建另一个 Java,并最终以某种方式嫁给我们的母亲)。但是,Clojure 可以随意借鉴好的想法,并且您可以看到受镜头启发的方法进入 Om 和 Enliven 等很酷的项目。
你必须小心地提出这样的问题,因为就像占据相同空间的同父异母的兄弟姐妹一样,Clojure 和 Haskell 必然会互相借鉴,并为谁是对的而争论不休。