Dus*_*etz 18 haskell functional-programming clojure immutability haskell-lens
我需要操作和修改深层嵌套的不可变集合(映射和列表),我想更好地理解不同的方法.这两个库解决了或多或少相同的问题,对吧?它们有何不同,哪种类型的问题更适合另一种?
J. *_*son 23
Clojure assoc-in允许您使用整数和关键字指定嵌套数据结构的路径,并在该路径中引入新值.它的合作伙伴dissoc-in,get-in和update-in其删除元素,让他们无需去除或分别修改它们.
镜头是双向编程的一个特定概念,您可以在其中指定两个数据源之间的链接,并且该链接允许您反映从一个到另一个的转换.在Haskell中,这意味着您可以构建镜头或类似镜头的值,将整个数据结构连接到其某些部分,然后使用它们将部件之间的变化传递给整体.
这里有个比喻.如果我们看一下assoc-in它的用法就像
(assoc-in whole path subpart)
Run Code Online (Sandbox Code Playgroud)
我们可以通过将其path视为镜头和assoc-in镜头组合来获得一些洞察力.以类似的方式编写(使用Haskell lens包)
set lens subpart whole
Run Code Online (Sandbox Code Playgroud)
让我们连接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
Run Code Online (Sandbox Code Playgroud)
这是相似之处的开始,但也有很大的不同.在许多方面,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
Run Code Online (Sandbox Code Playgroud)
现在我们可以使用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
Run Code Online (Sandbox Code Playgroud)
这显然有更多的语法噪音,但它表示对数据类型中的"路径"意味着更高程度的显式性,特别是它表示我们是否正在下降到数组或字典中.如果我们想要的话,我们可以通过Clj在Haskell类型类中实例化来消除一些额外的噪声Ixed,但是在这一点上它几乎不值得.
相反,要点assoc-in是适用于非常特殊的数据下降.由于Clojure的动态类型和重载,它比我上面列出的类型更通用IFn,但是一个非常类似的固定结构可以嵌入Haskell,只需要进一步的努力.
透镜可以更进一步,并且具有更高的类型安全性.例如,上面的例子实际上不是真正的"镜头",而是"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"
Run Code Online (Sandbox Code Playgroud)
我们还可以推广到同时针对多个相似子部分的镜头(真正的遍历)
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]
Run Code Online (Sandbox Code Playgroud)
甚至可以定位模拟的子部件,这些子部件实际上并不存在,但仍然形成我们数据的等效描述
eulerAnglePhi :: Lens Point Double
eulerAngleTheta :: Lens Point Double
eulerAnglePsi :: Lens Point Double
Run Code Online (Sandbox Code Playgroud)
从广义上讲,Lenses概括了Clojure *-in函数族抽象的整个值和值的子部分之间基于路径的相互作用.你可以在Haskell中做更多的事情,因为Haskell有一个更加发达的类型和镜头概念,作为第一类对象,广泛地概括了获取和设置的概念,这些概念只是简单地用*-in函数表示.
你说的是两件不同的事情.
您可以使用镜头来解决与assoc-in类似的问题,其中您使用的是与语义匹配的集合类型(Data.Map,Data.Vector)但存在差异.
在像Clojure这样的无类型语言中,通常根据具有非静态内容(哈希映射,向量等)的集合构建域数据,即使它是通常静态的建模数据.
在Haskell中,您将使用记录和ADT构建数据,在这里您可以表达可能存在或可能不存在的内容(或包装集合),但默认值是静态已知内容.
要查看的一个库是http://hackage.haskell.org/package/lens-aeson,其中包含可能具有不同内容的JSON文档.
这些示例表明,当您的路径和类型与结构/数据不匹配时,它会踢出一个Nothing而不是Just a.
除了提供声音吸气/设定器行为之外,镜头不会做任何事情.它没有表达对数据外观的特定期望,而assoc-in仅对具有可能非确定性内容的关联集合有意义.
这里的另一个区别是纯度和懒惰与严格和不纯的语义.在Haskell中,如果你从未使用过"旧"状态,只使用最新状态,那么只会实现该值.
tl; dr 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
Run Code Online (Sandbox Code Playgroud)
如果字符串不存在或为空,则不执行任何操作:
(_2._Right._Cons._1) (\_ -> getChar) (1,Left 5)
-- nothing read
(_2._Right._Cons._1) (\_ -> getChar) (1,Right "")
-- nothing read
Run Code Online (Sandbox Code Playgroud)