Conversation
adamgundry
left a comment
There was a problem hiding this comment.
Thanks for the PR! As I understand it, the key point here is to generalise [i]summing so it can be used with traversals and setters, since they admit a monoid as well. We should certainly offer this in some form.
Maybe we should have less polymorphic versions that constrain the returned optic kind,e.g.
tsumming :: (Is k A_Traversal, Is l A_Traversal) => Optic' k is s a -> Optic' l js s a -> Traversal' s aand perhaps we can even generalise this to Optic rather than Optic'? Though the accumulation of ...summing names does suggest a class like After might well be justified.
| -- Essentially this collapses the hierarchy into | ||
| -- Traversal, Fold, and Setter, | ||
| -- and then takes the usual Join | ||
| type family AfterJoin (k :: OpticKind) (l :: OpticKind) where |
There was a problem hiding this comment.
Perhaps I'm missing something, but is it the case that AfterJoin k l ~ Join A_Traversal (Join k l)? Couldn't we use that definition instead?
There was a problem hiding this comment.
I haven't thought deeply about it, but I guess that would make the trick, since A_Traversal is the "bottom" of that restricted hierarchy.
optics-core/src/Optics/Monoidal.hs
Outdated
|
|
||
| infixr 6 <++> | ||
| class After is m where | ||
| nothing :: Optic' m is s a |
There was a problem hiding this comment.
We already have ignored :: IxAffineTraversal i s s a b which is strictly more general than nothing, isn't it?
There was a problem hiding this comment.
Not really, if only because of the types. nothing here can be a Setter, Fold, or Traversal, everything more general than ignored.
There was a problem hiding this comment.
AffineTraversals are (i.e. can be cast to) Setters, Folds and Traversals.
There was a problem hiding this comment.
Right, I mean ignored is more general in terms of the Is relation. It doesn't strictly have a more general type, but optics-style polymorphism will allow it to be used with eliminators for setters, folds and traverals.
There was a problem hiding this comment.
castOptic ignored (and noIx (castOptic ignored) when explicit unindexed variant is needed) do the job.
| nothing :: Optic' m is s a | ||
| (<++>) :: (Is k m, Is l m, m ~ AfterJoin k l) | ||
| => Optic' k is s a -> Optic' l is s a | ||
| -> Optic' m is s a |
There was a problem hiding this comment.
In general, we tend to avoid introducing new operators in optics, or at least provide a non-operator equivalent. Is there precedent for this operator elsewhere? I suppose summing would be the obvious name choice if we don't mind a slightly breaking change.
There was a problem hiding this comment.
I didn't know about that rule. I haven't seen this operator used anywhere else, but I think that summing would work great here (and would provide a good backwards-compatibility story for those already using summing in Fold).
optics-core/src/Optics/Monoidal.hs
Outdated
|
|
||
| instance After NoIx A_Traversal where | ||
| nothing = traversalVL $ \_ s -> pure s | ||
| t1 <++> t2 = traversalVL $ \f s -> traverseOf t1 f s *> traverseOf t2 f s |
There was a problem hiding this comment.
I wonder if there is any benefit to implementing this directly on the profunctor representation rather than going via the VL representation...
optics-core/src/Optics/Monoidal.hs
Outdated
|
|
||
| instance After NoIx A_Traversal where | ||
| nothing = traversalVL $ \_ s -> pure s | ||
| t1 <++> t2 = traversalVL $ \f s -> traverseOf t1 f s *> traverseOf t2 f s |
There was a problem hiding this comment.
The *> is suspicious. What Setter made from this traversal does. (I suspect nothing to the first half).
Yet, the After NoIx A_Setter instance does something else.
This feels like a giving shotgun to a kid.
There was a problem hiding this comment.
Right, I see that there is a discrepancy between what the instances for Setter and Traversal do.
|
|
||
| instance After NoIx A_Setter where | ||
| nothing = sets (\_ s -> s) | ||
| s1 <++> s2 = sets (\f s -> over s2 f (over s1 f s)) |
There was a problem hiding this comment.
There should be a big warning. The resulting Setter is lawful only(?) if halves of it don't modify the same parts of underlying structure.
I think that in this case, <++> have to be commutative operator. Yet for folds that doesn't make sense.
This structure feels very ad-hoc to me. I'm not sure this is good idea at all to overload this (or summing or ...) name.
There was a problem hiding this comment.
That is exactly the use case I had in mind. Imagine that I want to define several Traversals which works over binary trees. With this API I could define it as:
data BinaryTree a = Leaf { _leaf :: a } | Node { _left, _right :: BinaryTree a }
preOrder = leaf <++> left % preOrder <++> right % preOrder
inOrder = left % preOrder <++> leaf <++> right % preOrder
postOrder = left % preOrder <++> right % preOrder <++> leafThere was a problem hiding this comment.
This is a nice example use case, and could do with appearing in the documentation, whatever API variant we end up with. 😄
There was a problem hiding this comment.
Let me highlight the disrepacy of traversal to setter.
Prelude Optics Optics.Monoidal Data.Char> data BinaryTree a = Leaf { _leaf :: a } | Node { _left, _right :: BinaryTree a } deriving Show
Prelude Optics Optics.Monoidal Data.Char> makeLenses ''BinaryTree;
Prelude Optics Optics.Monoidal Data.Char> let preOrder :: Traversal' (BinaryTree a) a; preOrder = leaf <++> (left % preOrder) <++> (right % preOrder)-- traversal as fold, works
Prelude Optics Optics.Monoidal Data.Char> toListOf preOrder $ Node (Leaf 'x') (Node (Leaf 'y') (Leaf 'z'))
"xyz"
-- traversal as setter, doesn't work
Prelude Optics Optics.Monoidal Data.Char> over preOrder toUpper $ Node (Leaf 'x') (Node (Leaf 'y') (Leaf 'z'))
Node {_left = Leaf {_leaf = 'x'}, _right = Node {_left = Leaf {_leaf = 'y'}, _right = Leaf {_leaf = 'z'}}}
-- traversal as traversal is not lawful.Prelude Optics Optics.Monoidal Data.Char> let preOrderSetter :: Setter' (BinaryTree a) a; preOrderSetter = leaf <++> (left % preOrderSetter) <++> (right % preOrderSetter)
-- expected
Prelude Optics Optics.Monoidal Data.Char> toListOf preOrderSetter $ Node (Leaf 'x') (Node (Leaf 'y') (Leaf 'z'))
<interactive>:45:1: error:
• A_Setter cannot be used as A_Fold
Perhaps you meant one of these:
-- as setter, works
Prelude Optics Optics.Monoidal Data.Char> over preOrderSetter toUpper $ Node (Leaf 'x') (Node (Leaf 'y') (Leaf 'z'))
Node {_left = Leaf {_leaf = 'X'}, _right = Node {_left = Leaf {_leaf = 'Y'}, _right = Leaf {_leaf = 'Z'}}}Current summing would get you Fold behaviou
r already.
To compose traversals, you need to do so something like adjoin
in https://hackage.haskell.org/package/lens-4.19.2/docs/Control-Lens-Unsound.html
FWIW: I don't like Unsound name. There's nothing "unsound" as such,
just that the preconditions are very easy to violate (c.f. lens constructor/introduction has preconditions too).
So I would rename <++> to adjoin, implement it as in lens (so Traversal degenerates to proper Fold and Setters). Add a warning sign, and not export by default.
- Adam's comment, I do agree with them.
optics-core/src/Optics/Monoidal.hs
Outdated
|
|
||
| infixr 6 <++> | ||
| class After is m where | ||
| nothing :: Optic' m is s a |
There was a problem hiding this comment.
AffineTraversals are (i.e. can be cast to) Setters, Folds and Traversals.
|
@phadej thanks for catching the Traversal/Setter difference and the unlawfulness. I buy that |
|
@adamgundry you can break Prelude Optics Optics.Monoidal Data.Char> let setter = castOptic @A_Setter simple <++> castOptic @A_Setter simple
Prelude Optics Optics.Monoidal Data.Char> over setter (+1) . over setter (*2) $ (3 :: Int)
14
Prelude Optics Optics.Monoidal Data.Char> over setter ((+1) . (*2)) $ (3 :: Int)
15 |
|
This is similar to what I tried to do in #91 (comment) Combination of results with read-only optics works out because you can discard the structure you traverse, so What |
|
I have just pushed a new attempt, which satisfies the traversal laws if the traversals focus on different parts of the value. *Optics Optics.Monoidal Data.Char> data BinaryTree a = Leaf { _leaf :: a } | Node { _left, _right :: BinaryTree a } deriving Show
*Optics Optics.Monoidal Data.Char> makeLenses ''BinaryTree;
*Optics Optics.Monoidal Data.Char> let preOrder :: Traversal' (BinaryTree a) a; preOrder = leaf <++> (left % preOrder) <++> (right % preOrder)
*Optics Optics.Monoidal Data.Char> toListOf preOrder $ Node (Leaf 'x') (Node (Leaf 'y') (Leaf 'z'))
"xyz"
*Optics Optics.Monoidal Data.Char> over preOrder toUpper $ Node (Leaf 'x') (Node (Leaf 'y') (Leaf 'z'))
Node {_left = Leaf {_leaf = 'X'}, _right = Node {_left = Leaf {_leaf = 'Y'}, _right = Leaf {_leaf = 'Z'}}} |
|
Note that my end goal is also to provide a generalized interface to |
|
I'm not convinced that introducing a type class for this is a good idea. The only non-trivial instances currently are for Since As a side note, I'm curious how well |
|
After some experimentation I got this. Seems to optimize very well (existing version that calls partsOf also optimizes well for small examples, but this one has much less code after inlining takes place so should compile faster and potentially optimize better for non-trivial cases). adjoin
:: (Is k A_Traversal, Is l A_Traversal)
=> Optic' k is s a
-> Optic' l js s a
-> Traversal' s a
adjoin o1 o2 = combined % traversed
where
combined = lensVL $ \f s -> evalState ( traverseOf o1 update s
>>= traverseOf o2 update)
<$> f ( toListOf (castOptic @A_Traversal o1) s
++ toListOf (castOptic @A_Traversal o2) s)
update a = get >>= \case
a' : as' -> put as' >> pure a'
[] -> pure a
{-# INLINE adjoin #-}
-- indexed variant
iadjoin
:: (Is k A_Traversal, Is l A_Traversal, is `HasSingleIndex` i)
=> Optic' k is s a
-> Optic' l is s a
-> IxTraversal' i s a
iadjoin o1 o2 = conjoined (adjoin o1 o2) (combined % traversed % itraversed)
where
combined = lensVL $ \f s -> evalState ( traverseOf o1 update s
>>= traverseOf o2 update)
<$> f ( itoListOf (castOptic @A_Traversal o1) s
++ itoListOf (castOptic @A_Traversal o2) s)
update a = get >>= \case
(_, a') : as' -> put as' >> pure a'
[] -> pure a
{-# INLINE iadjoin #-} |
|
One more argument against the type class: things like That's the reason we have |
|
@arybczak thanks for looking at this. I'd be happy with We should have "Monoid structure" sections of the |
|
Given this discussion, I am going to close the PR, since clearly is should not be merged as is. Let me know if I could help bringing the other operators to |
The idea is to generalize the
summingoperation inData.Fold, so that it give stronger guarantees. For example, if you sum two traversals, you can get a traversal out of it.I have also included an example of usage to return elements from a
Treeon increasing level.