Integrate witherable and lens.
Based on the ideas from https://chrispenner.ca/posts/witherable-optics
but with a more combinator-based approach to restore full
compatibility with lens combinators like set
and
over
. Additionally, this approach gets rid of Maybe
wrappers in
results that are always Just
.
This library is based on a variant of Traversal
s with the type
forall f. Applicative f => ((a-> Withering f b) -> s -> f t)
. Note
that this type is absolutely not an optic due to the Withering
wrapper, but it's really close. It composes on either side with lenses
and traversals with (.)
and that composition maintains the filtering
type. No type alias is provided for this type because type aliases
containing a forall
cause problems with subsumption.
The core tool for creating values with these types is
withered
:: (Applicative f, Witherable t)
=> (a -> Withering f b) -> t a -> f (t b)
One tool for working with the types is
mapMaybeOf
:: ((a -> Withering Identity b) -> s -> Identity t)
-> (a -> Maybe b) -> s -> t
These can be used like such
ghci> let z = M.fromList [('a', ["1", "2", "3"]), ('b', ["4", "Alpaca", "6"])]
ghci> z & mapMaybeOf (traverse . withered) (readMaybe :: String -> Maybe Int)
fromList [('a',[1,2,3]),('b',[4,6])]
ghci> z & mapMaybeOf (withered . traverse) (readMaybe :: String -> Maybe Int)
fromList [('a',[1,2,3])]
As those results demonstrate, the location of the withered
combinator controls where the pruning stops. It functions as a kind of
catch combinator in this sense.
Of course, filterOf
is provided as well
filterOf
:: ((a -> Withering Identity a) -> s -> Identity s)
-> (a -> Bool) -> s -> s
ghci> [[1, 2, 3], [4, 5, 6]] & filterOf (traverse . withered) even
[[2],[4,6]]
Traversals that might fail can be combined with the decayed
combinator to cause them to completely remove a value that they
otherwise would ignore
ghci> [('a', Right 1), ('b', Left 2), ('c', Left 3)] & filterOf (withered . _2 . _Left) even
[('a',Right 1),('b',Left 2)]
ghci> [('a', Right 1), ('b', Left 2), ('c', Left 3)] & filterOf (withered . _2 . (_Left `failing` decayed)) even
[('b',Left 2)]
In the first case, the 'a'
value is returned because the filter
doesn't find any non-even values under the Right
value when it's
looking under the _Left
prism. In the second case, the failure of
the _Left
prism results in falling back on decayed
to remove the
branch.
Compatibility with normal lens operations can be restored with
unwithered
. This can be especially useful when combined with
guarded
, which acts somewhat like lens's filtered
but it prunes
the structure up to the next withered
.
unwithered :: Functor f => (a -> f b) -> a -> Withering f b
guarded
:: Applicative f
=> (a -> Bool) -> (a -> Withering f b)
-> a -> Withering f b
ghci> [[1,2],[],[1,3,9],[],[],[6,4,7],[]] & withered . guarded (not . null) . unwithered . traverse +~ 10
[[11,12],[11,13,19],[16,14,17]]
Note that pruning out the empty lists happens in-line with the
modification of the other elements. An additional combinator is
provided combining withered
and unwithered
for when you want to
change the pruning depth
rewithered
:: (Applicative f, Witherable t)
=> (a -> Withering f b) -> t a -> Withering f (t b)
ghci> [[1,2],[],[1,3,9],[],[],[6,4,7],[]] & (withered . guarded (not . null) . rewithered . guarded even . unwithered) +~ 10
[[12],[],[16,14]]
In that example, the first guarded
strips out null lists in the
input. The rewithered
descends into the sublists and sets each of
them as the catch point for later pruning. The second guarded
strips
out all non-even entries in each list. The unwithered
restores
compatibility with lens combinators like (+~)
, which is used to
modify every remaining focused value. Note the presence of the []
in
the output list. The guarded
combinator violates the lens laws in
the same manner as filtered
, where behavior might change with
refactoring. This doesn't mean it's dangerous to use, merely that you
have to pay attention to changes in behavior when refactoring chains
involving it.