Search code examples
haskellhaskell-lenslensesnewtype

How to eliminate the boilerplate of wrapping and unwrapping using lenses


tl;dr: is it possible to use any of the lens family of abstractions to wrap/unwrap any arbitrary newtype (that provides an instance for such abstractions)?

I'll motivate my question by a simple example, based on a true story. Suppose I define the following newtype:

newtype FreeMonoid a = FreeMonoid { asMap :: Map a Int }

which is used to represent terms of the form:

a0 <> a1 <> ... <> an-1

We can represent free-monoids as lists:

instance Ord a => IsList (FreeMonoid a) where
    type Item (FreeMonoid a) = a
    fromList xs = FreeMonoid $ Map.fromListWith (+) $ zip xs (repeat 1)
    toList (FreeMonoid p) = do
        (x, n) <- Map.toList p
        genericReplicate n x

Two examples of free-monoids are sequences of sum and sequences of products:

type FreeSum a = FreeMonoid (Sum a)
type FreeProduct a = FreeMonoid (Product a)

Where Sum and Product are defined in Data.Monoid. Now we could define fromList and toList operations for FreeSum and FreeProduct as follows:

fromListSum :: Ord a => [a] -> FreeSum a
fromListSum = fromList . (Sum <$>)

fromListProduct :: Ord a => [a] -> FreeProduct a
fromListProduct = fromList . (Product <$>)  

But this has quite a lot of boilerplate. It'd be nicer if we could simply say:

fromListW :: (Ord a, Wrapper f) => [a] -> FreeMonoid (f a)
fromListW = fromList . (wrap <$>)

where wrap is some operation of the (hypotetical) Wrapper class were:

wrap :: a -> f a
unwrap :: f a -> a

Similarly, I'd like to be able to write a function:

toListW :: (Ord a, Wrapper f) => FreeMonoid (f a) -> [a]
toListW = (unwrap <$>) . toList

Lenses seem to provide such an abstraction in Control.Lens.Wrapped (for which Sum and Product in this example are instances of the typeclasses there!). However my attempts to understand and use the abstractions in this module have failed. For instance:

fromListW :: (Ord a, Wrapped (f a))  => [a] -> FreeMonoid (f a)
fromListW = fromList . (Wrapped <$>)

won't work since the argument is not a list of Unwrapped (f a).

So my question is:

  • Do lenses provide an abstraction similar to this Wrapper class?
  • If not, can this "scrap-your-boilerplate" problem be solved by using lenses?

Solution

  • The "problem" is that you're using Wrapped, which is really meant to be a convenience pattern synonym and not a wrapping "constructor". Because it's designed to support polymorphic wrapping, you need to assert that your type can be rewrapped:

    fromListW :: (Rewrapped a a, Ord a) => [Unwrapped a] -> FreeMonoid a
    fromListW = fromList . (Wrapped <$>)
    

    This then works as expected:

    > let x = [1,2,3]
    > fromListW x :: FreeMonoid (Sum Int)
    FreeMonoid {asMap = fromList [(Sum {getSum = 1},...
    > fromListW x :: FreeMonoid (Product Int)
    FreeMonoid {asMap = fromList [(Product {getProduct = 1},...
    >
    

    I think a more idiomatic lens implementation would be:

    fromListW :: (Rewrapped a a, Ord a) => [Unwrapped a] -> FreeMonoid a
    fromListW = fromList . view (mapping _Unwrapped)
    

    This still requires the Rewrapped a a constraint, but you can use the non-polymorphic _Unwrapped' instead:

    fromListW :: (Wrapped a, Ord a) => [Unwrapped a] -> FreeMonoid a
    fromListW = fromList . view (mapping _Unwrapped')
    

    which looks a little more natural.

    The toListW implementation would have similar structure:

    toListW :: (Wrapped a, Ord a) => FreeMonoid a -> [Unwrapped a]
    toListW = view (mapping _Wrapped') . toList