我相信在 Haskell 中,您的组合器大致正确的想法是
merge :: Lens' s t -> Lens' s t -> Lens' s (t, t)
可能概括为
merge :: Lens' s t -> Lens' s t' -> Lens' s (t, t')
这样您的两个目标可能在类型上有所不同。我们可以如下“实现”它,但这样做会暴露一个问题
merge l1 l2 = lens pull push where
pull s = (view l1 s, view l2 s)
push s (t1, t2) = set l2 t2 (set l1 t1 s)
特别是,在Setter
这个等式的一边,我们明确地排序了我们放回我们的价值观的方式。这意味着我们可以使用它merge
来轻松构建违反镜头定律的镜头。特别是,它们违反了 PUT-GET 法。这是一个反例
newtype Only a = Only a
-- A lens focused on the only slot in Only
only :: Lens' (Only a) a
only inj (Only a) = Only <$> inj a
-- We merge this lens with *itself*
testLens :: Lens' (Only a) (a, a)
testLens = merge only only
-- and now it violates the lens laws
> view testLens (Only 1 & testLens .~ (1,2))
(2,2)
或者,用简单的英语来说,如果我们merge
是一个镜头本身,那么Setter
侧面会在同一位置变成两个连续的集合。这意味着如果我们尝试使用一对不同的值设置它,则只有第二个设置会持续存在,因此我们违反了 PUT-GET 法则。
该lens
库倾向于避开无效的镜头,因此该组合器不可用。我们最接近的是alongside
具有以下(受限)类型
alongside :: Lens' s a
-> Lens' s' a'
-> Lens' (s,s') (a,a')
但是,如您所见,这确保了我们的源也是一种产品类型,因此Setter
s 唯一地适用于源的每一侧。如果我们尝试编写一个dup
Lens
可以组合alongside
构建的程序merge
,我们将遇到与以前相同的问题
dup :: Lens' a (a, a) -- there are two possible implementations, neither are lenses
dup1 inj a = fst <$> inj a
dup2 inj a = snd <$> inj a
现在,从 Ruby 的角度来看,所有这些都可能是相当技术性且毫无意义的。在 Ruby 中,您不会让编译器强制执行类型安全,因此您可能会将许多规则混合在一起并降低严格性。在这种情况下,merge
使用其顺序写入语义来实现可能是有意义的。
实际上,根据您给出的示例,我指出的反例似乎在 Ruby 中甚至都不会发生,因为您的目标类型是 Hash 并且已经确保了唯一键。
但重要的是要注意,merge
如果你足够努力地观察它,它可以用来制造不合适的镜片。从根本上说,这意味着在更复杂的场景中,使用merge
很容易导致奇怪的错误,因为它违反了我们对镜头的直觉。这对 Ruby 来说可能不是一个大问题,因为无论如何复杂的抽象在 Ruby 社区中都是不受欢迎的,但这就是我们有法律的原因。