这是在Haskell中使用拉链的一个例子:
data Tree a = Fork (Tree a) (Tree a) | Leaf a
data Cxt a = Top | L (Cxt a) (Tree a) | R (Tree a) (Cxt a)
type Loc a = (Tree a, Cxt a)
left :: Loc a -> Loc a
left (Fork l r, c) = (l, L c r)
right :: Loc a -> Loc a
right (Fork l r, c) = (r, R l c)
top :: Tree a -> Loc a
top t = (t, Top)
up :: Loc a -> Loc a
up (t, L c r) = (Fork t r, c)
up (t, R l c) = (Fork l t, c)
upmost :: Loc a -> Loc a
upmost l@(t, Top) = l
upmost l = upmost (up l)
modify :: Loc a -> (Tree a -> Tree a) -> Loc a
modify (t, c) f = (f t, c)
这是在Clojure中使用拉链的一个例子:
(use 'clojure.zip)
(require '[clojure.zip :as z])
user> (def z [[1 2 3] [4 [5 6] 7] [8 9]])
#'user/z
user> (def zp (zipper vector? seq (fn [_ c] c) z))
#'user/zp
user> zp
[[[1 2 3] [4 [5 6] 7] [8 9]] nil]
user> (-> zp down)
[[1 2 3] {:l [], :pnodes [[[1 2 3] [4 [5 6] 7] [8 9]]], :ppath nil, :r ([4 [5 6] 7] [8 9])}]
user> (first (-> zp down))
[1 2 3]
这是在Haskell中使用Lens的一个例子:
data Person = P { name :: String
, addr :: Address
}
data Address = A { street :: String
, city :: String
, postcode :: String
}
setPostcode :: String -> Person -> Person
setPostcode pc p = p { addr = addr p { postcode = pc }}
这是在Clojure中使用镜头的一个例子。
(use 'lens)
(defrecord Address [street city postcode])
(defrecord Person [name age address])
(defrecord User [uid username identity password])
(def -postcode (mklens :postcode))
(def -city (mklens :city))
(def -street (mklens :street))
(def -address (mklens :address))
(def -age (mklens :age))
(def -name (mklens :name))
(def -uid (mklens :uid))
(def -username (mklens :username))
(def -identity (mklens :identity))
(def -password (mklens :password))
(-get -postcode home)
(-set -postcode home 500)
现在看来透镜和拉链都是遍历嵌套数据结构的功能方式。
我的问题是:镜片和拉链之间有什么区别?是否适合特定用例?
答案 0 :(得分:26)
拉链类似于游标:它们允许以有序方式遍历树。他们通常的操作是up
,down
,left
,right
和edit
。 (名称可能因impl而异)
镜头是某种通用键(如“关联数据结构的键”)。结构不需要订购。他们通常的操作是fetch
和putback
,与get
和assoc
非常相似。 (名称可能因impl而异)
因此,当您看到拉链非常关注层次结构(上/下)和顺序(左/右)时,镜头只是关注一个数据(因此名称),甚至可能是一个投影(这是原始结构中不存在的东西。)
例如,在我正在进行的Enliven工作中,我的镜头可以让我专注于HTML文档中的单个类或样式属性。
答案 1 :(得分:11)
Zippers是数据类型的变体,它在所有方向上将类型展开到本地上下文及其范围中。在Zipper上,您可以实现高效的动作和本地更新。
镜头是数据类型特定组件的一流检查。它们专注于数据结构的0,1或许多子部分。值得注意的是,你在Haskell中使用镜头的例子实际上不是镜头 - 它不是头等舱。
构建一个专注于拉链某些部分的镜头是完全合理的。例如,比您的示例更简单的拉链是缺点列表拉链
data Cons a = Empty | Cons a (Cons a)
data ConsZ a = ConsZ { lefts :: Cons a; here :: a; rights :: Cons a }
zip :: Cons a -> Maybe (ConsZ a)
zip Empty = Nothing
zip (Cons a as) = ConsZ Empty a as
unzip :: ConsZ a -> Cons a
unzip (ConsZ Empty a as) = Cons a as
unzip (ConsZ (Cons l ls) a as) = unzip (ConsZ ls) l (Cons a as)
我们可以逐步修改此结构,向左或向右移动焦点
moveRight :: ConsZ a -> Maybe (ConsZ a)
moveRight (ConsZ _ _ Empty) = Nothing
moveRight (ConsZ ls x (Cons a as)) = ConsZ (Cons x ls) a as
并修改当前的本地点
modify :: (a -> a) -> ConsZ a -> ConsZ a
modify f (ConsZ ls x rs) = ConsZ ls (f x) rs
我们还可以制造可以进入拉链结构各个部分的镜片
type Lens s a = forall f . Functor f => (a -> f a) -> (s -> f s)
_lefts :: Lens (ConsZ a) a
_lenfs inj (ConsZ ls x rs) = (\ls -> ConsZ ls' x rs) <$> inj ls
_here :: Lens (ConsZ a) a
_here inj (ConsZ ls x rs) = (\x' -> ConsZ ls x' rs) <$> inj x
甚至用它们来有效地构建我们的拉链动作
over :: ((a -> Identity a) -> s -> Identity s) -> (a -> a) -> (s -> s)
over l f s = runIdentity (l (Identity . f) s)
modify = over _here
然而,最终,镜头始终是对数据结构中特定点的第一类访问。它们可以组成,给人以“运动”的幻觉。在一个类型中,但如果你真的想要那么你应该进行拉链转换并使用真正的拉链类型。
答案 2 :(得分:1)
镜片和拉链并不是互相看待世界的方式。你可以建立一个可移动的焦点&#34;数据类型在镜头顶部,通过将一系列镜头作为类型对齐的堆叠来实现。跟踪您在结构中所访问的类型意味着您知道在重新启动时您将看到的类型。
此&#34;可移动焦点的API&#34;看起来大致如下:
empty :: Path (E :> a)
up :: Path (as :> a :> b) -> Path (as :> a)
down :: Path (as :> a) -> Traversal' a b -> Path (as :> a :> b)
left :: Path (as :> a :> b) -> Path (as :> a :> b)
right :: Path (as :> a :> b) -> Path (as :> a :> b)
flatten :: Path as -> Traversal' (Top as) (Bottom as)
Path
由类型的snoc列表参数化。当前焦点的类型&#34; Path
的列表是列表中最右边的元素。
如果Path
关注某个结构中的a
,您可以使用down
附加Traversal' a b
,以获取Path
侧重于b
(即Traversal
的第一个结果)。然后,您可以返回up
,弹出最近添加的Traversal
,以便返回Path
,再次关注a
。 left
和right
将焦点移到最顶层Traversal
内。
您还需要一种方法将Path
变回实际的Traversal
,以便访问Path
放大的实际值。 flatten
组合器就是这样做的。 Top
和Bottom
是一对类型系列,分别找到snoc-list的最左边和最右边的元素。
那么它是如何实现的?
infixl 5 :>
data Snoc a = E | Snoc a :> a
type family Top as where
Top (E :> a) = a
Top (as :> _) = Top as
type family Bottom as where
Bottom (_ :> a) = a
data Path as where
Top :: Path (E :> a)
Child :: Path (as :> a) -> Traversal' a b -> Int -> Path (as :> a :> b)
Path
是一个堆叠形的GADT。 Top
构造函数创建一个空Path
- 从任何值到自身的路径。 Child
构造函数侧重于Traversal
的特定元素 - 它包含父Path
,其中a
,Traversal
来自a
} b
和Int
代表Traversal
所关注的Path
的特定元素。
empty
创建一个空路径。
empty :: Path (E :> a)
empty = Top
up
采用非空路径(由类型保证)并弹出最顶层的Traversal
。
up :: Path (as :> a :> b) -> Path (as :> a)
up (Child p _ _) = p
down
需要Traversal
并将其附加到Path
,重点关注Traversal
的最左边结果。
down :: Path (as :> a) -> Traversal' a b -> Path (as :> a :> b)
down p t = Child p t 0
left
和right
不会改变您重点关注的结构级别 - 不添加或删除堆栈中的遍历 - 它们只是更改了最顶层的遍历路径指向。
left :: Path (as :> a :> b) -> Path (as :> a :> b)
left (Child p t n) = Child p t (n - 1)
right :: Path (as :> a :> b) -> Path (as :> a :> b)
right (Child p t n) = Child p t (n + 1)
flatten
依次查看每个遍历并使用elementOf
来关注遍历的特定元素。它使用.
将它们组合在一起。
flatten :: Path as -> Traversal' (Top as) (Bottom as)
flatten Top = id
flatten (Child p t n) = flatten p . elementOf t n
Path
完全不是拉链。拉链拉链的一个重要部分是,您可以有效地查看或编辑焦点及其邻居,而无需遍历或重建整个事物。 Path
只是在不参考特定结构的情况下编写遍历,因此它与使用整个遍历一样低效。
但是,从Path
到真正的拉链并不是一个很大的飞跃。 The zippers
package提供真正的拉链 - 光标可以有效地访问实际结构的焦点部分 - 一般来说,基于类型对齐的镜头序列的这种想法。当你通过一个结构时,Zipper
将每个遍历解压缩到一个数据结构中,就像你的Loc
一样。然后,当您返回upward
时,它会使用Traversal
将新值写回到结构中。
答案 3 :(得分:0)
lens
是 some 数据结构中的路径。您可以组成这些路径,就像可以通过创建列表树的列表等来组成数据结构一样。类型对齐在任何阶段都由类型系统验证:您可以为数据结构提供一个镜头,使用其他人们的镜头。每个人都依赖于本机编译器的权限。
zipper
是固定的静态数据结构内的可移动焦点。将构成物按类型对齐的顺序进行序列化可以让您重新掌控,同时仍然确保最终完成,因此您可以在已建立的操作序列中添加操作。但是用于此类操作的词汇left
,right
,up
,down
是来自所述固定数据结构的词汇derived。