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 Traversals 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.