I believe that roughly the right idea for your combinator, in Haskell, is
merge :: Lens' s t -> Lens' s t -> Lens' s (t, t)
possibly generalized to
merge :: Lens' s t -> Lens' s t' -> Lens' s (t, t')
so that your two targets may differ in type. We could "implement" this as follows, but doing so will reveal a problem
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)
In particular, on the Setter
side of this equation we explicitly order the way we put back our values. This means that we can use merge
to easily build lenses which violate the lens laws. In particular, they violate the PUT-GET law. Here's a counterexample
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)
Or, in plain english, if we merge
a lens with itself then the Setter
side turns into two sequential sets on the same location. This means that if we try to set it with a pair of values which are different only the second set will persist and thus we violate the PUT-GET law.
The lens
library tends to eschew invalid lenses so this combinator isn't available. The closest we have is alongside
which has the following (restricted) type
alongside :: Lens' s a
-> Lens' s' a'
-> Lens' (s,s') (a,a')
but, as you can see, this ensures that our source is also a product type so that the Setter
s apply uniquely to each side of the source. If we were to try to write a dup
Lens
which we could compose with alongside
to build merge
we'll run into the same problem as before
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
Now, all of this might be fairly technical and meaningless from a Ruby perspective. In Ruby you're not going to have compiler enforced type safety and so you'll likely blend a lot of the rules together and achieve less strictness. In such a case, it might be meaningful to implement merge
with its sequential writes semantics.
In fact, based on the examples you've given, it looks like the counterexample I pointed out cannot even happen in Ruby since your target type is a Hash and ensures unique keys already.
But it's important to note that merge
can be used to build improper lenses if you look at it hard enough. Fundamentally what that means is that in more complex scenarios, using merge
can easily lead to weird bugs caused by it violating our intuitive sense for what a lens is. This may not be a huge problem for Ruby as complex abstraction is frowned upon in the Ruby community anyway, but that's why we have the laws.