我需要操作和修改深层嵌套的不可变集合(地图和列表),我想更好地理解不同的方法。这两个库解决了或多或少相同的问题,对吧?它们有何不同,哪种类型的问题更适合另一种?
答案 0 :(得分:23)
Clojure的assoc-in
允许您使用整数和关键字指定嵌套数据结构的路径,并在该路径中引入新值。它有合作伙伴dissoc-in
,get-in
和update-in
,可以删除元素,不删除它们或分别修改它们。
镜头是双向编程的一个特殊概念,您可以在其中指定两个数据源之间的链接,并且该链接允许您反映从一个到另一个的转换。在Haskell中,这意味着您可以构建镜头或类似镜头的值,将整个数据结构连接到其中的某些部分,然后使用它们将更改从部件传输到整体。
这里有个比喻。如果我们看一下assoc-in
的用法,就像
(assoc-in whole path subpart)
我们可以通过将path
视为镜头而将assoc-in
视为镜头组合来获得一些洞察力。以类似的方式编写(使用Haskell lens
包)
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
比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.Map
,Data.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-in
,get-in
,update-in
等有点像关联数据结构的镜头。在Clojure中有一般的镜头实现。他们为什么不一直使用?这是哲学上的一个不同之处(也许是令人毛骨悚然的感觉,与所有的制定者和吸气者一起,我们将在Clojure中制作另一个Java,并以某种方式最终与我们的母亲结婚)。但是,Clojure可以自由地借用好的想法,你可以看到镜头启发的方法进入像Om和Enliven这样的酷项目。
你必须小心地提出这样的问题,因为像兄弟姐妹一样占据同一个空间的Clojure和Haskell必然会互相借用并争论谁是对的。