Search code examples
haskellhaskell-lens

Building complex functions with lens library


I'm trying to implement a function that has a lens argument and will mconcat a Maybe monoid with two applicative effects of type (->) r. I am probably missing some basics, so any help in decomposing this problem is appreciated.

I want to write 'user' code that avoids passing the same arguments, something like this:

z pom root [(textFrom "groupId"), (textFrom "artifactId"), (textFrom "version")]

textFrom :: Text -> Document -> Lens' Document Element -> Maybe Text
textFrom name pom ln = listToMaybe $ pom ^.. ln ./ ell name . text

Here is what I tried. Concept without lens:

Prelude Data.Monoid Control.Applicative> let f a b = Just $ show (a+1) ++ b
Prelude Data.Monoid Control.Applicative> let z a b xs = mconcat $ ($ b) <$> ($ a) <$> xs
Prelude Data.Monoid Control.Applicative> z 3 "x" [f,f]
Just "4x4x"

With lens:

z :: (forall f. Functor f, Monoid b)  => Document -> ((Element -> f Element) -> Document -> f Document) -> [Document -> ((Element -> f Element) -> Document -> f Document) -> b] -> b
z pom ln xs = mconcat $ ($ ln) <$> ($ pom) <$> xs

But it fails to compile:

    Couldn't match expected type ‘[a0
                               -> Document
                               -> [Document
                                   -> ((Element -> f Element) -> Document -> f Document) -> b]
                               -> b]’
            with actual type ‘(Element -> f Element) -> Document -> f Document’
Relevant bindings include
  xs :: (Element -> f Element) -> Document -> f Document
    (bound at Main.hs:27:10)
  z :: (forall (f1 :: * -> *). Functor f1)
       -> Monoid b =>
          Document
          -> ((Element -> f Element) -> Document -> f Document)
          -> [Document
              -> ((Element -> f Element) -> Document -> f Document) -> b]
          -> b
    (bound at Main.hs:27:1)
Probable cause: ‘xs’ is applied to too few arguments
In the second argument of ‘(<$>)’, namely ‘xs’
In the second argument of ‘($)’, namely ‘($ ln) <$> ($ pom) <$> xs’

Solution

  • Sometimes, storing lenses in containers can cause issues but we can circumvent these by using ALens'. Conceptually, ALens' Element Document is more or less the same as Functor f => ((Element -> f Element) -> Document -> f Document), but you can put it into a container with fewer problems. An important thing to note is that each lens is universally qualified over all Functors, so the original signature should actually look more like this (although this won't quite work):

    z :: (Monoid b)
      => Document
      -> (forall f. Functor f => (Element -> f Element) -> Document -> f Document)
      -> [Document -> (forall f. Functor f => (Element -> f Element) -> Document -> f Document) -> b]
      -> b
    

    If we give z a type using ALens', we end up with this (assuming you are using the lens library. If not, see the note at the bottom):

     z :: (Monoid b)
       => Document
       -> ALens' Element Document
       -> [Document -> ALens' Element Document -> b] 
    

    With this new signature, the original definition that you gave will work.

    We can simplify this definition using a different Monoid instance. The Monoid b => (a -> b) Monoid instance combines functions that have the a Monoid result type. Here is an example:

    lengthSum :: [a] -> Sum Int
    lengthSum xs = Sum (length xs)
    
    λ> (lengthSum <> lengthSum) "abc"
    Sum {getSum = 6}
    

    The (<>) method mappends the results of applying each function to the given argument (so it ends up being essentially the same as length "abc" + length "abc"). Likewise, mconcat will combine the results of a list of functions. This also extends to functions of more than one argument, as long as each function is the same type and the result type is an instance of Monoid.

    With this instance in hand, we can define:

    z pom ln xs = mconcat xs pom ln
    

    This definition works with both the lens and non-lens version of the type.

    If you are not using the lens library, you should be able to define something like ALens' as

    newtype ALens s t a b = ALens
      { getLens :: forall f. Functor f => (a -> f b) -> s -> f t
      }
    type ALens' s a = ALens s s a a