Applicative style programming with profunctors

Posted on December 23, 2016

Quite unusually, this post is not written in Literate Haskell.

Applicative style

Applicative functors allow less expressiveness than monads, but are more general: every monad is an applicative functor.

A commonly mentionned benefit of programming with applicative functors is that they allow more optimizations to take place, in particular because the applicative product (<*>) can execute its two arguments “in parallel”.

(<*>) :: Applicative f => f (a -> b) -> f a -> f b

In contrast, the monadic bind (>>=) expects an arbitrary function as its second argument, which is quite opaque. This forces a sequential execution, where the first operand must be evaluated so that the second one can be applied to its result.

(>>=) :: Monad f => f a -> (a -> f b) -> f b

Applicative style relies on the Applicative and Alternative type classes; the former is actually a superclass of the latter.

Can we use this style to program bidirectionally with profunctors? In fact, my previous posts already show how to generalize Applicative. Here we shall focus on extending that generalization to work with Alternative.

Outline of naive parsers and printers

A parser typically uses Alternative to parse a sum type, with one branch for each constructor.

data Term
  = Var Int
  | Lambda Int Term
  | App Term Term

parseTerm :: Parser Term
parseTerm
  =   parseVar
  <|> parseLambda
  <|> parseApp

A printer decides the branch to take by pattern matching.

printTerm :: Term -> String
printTerm t =
  case t of
    Var _ -> printVar t
    Lambda _ _ -> printLambda t
    App _ _ -> printApp t

Bidirectional programming

Let us imagine we have invertible parsers for each alternative, merging parseVar and printVar, etc.

data IParser x a

var, lambda, app :: IParser Term Term

The final parser would look like this:

term :: IParser Term Term
term
  =   isVar    =? var
  <|> isLambda =? lambda
  <|> isApp    =? app

with some yet unknown operator (=?) and functions isVar, isLambda, isApp.

Filter

Each branch should filter the input term, such that if it doesn’t have the right constructor, then the current branch fails and control flows to the next branch.

Filtering can obviously enough be done with a boolean predicate.

isVar (Var _) = True
isVar _ = False

...

Then, we expect a signature similar to the standard filter on lists. The naive solution is just to put this in a type class.

class Profunctor p => Filterable p where
  (=?) :: (x -> Bool) -> p x a -> p x a

But this lacks the elegance of previous “high-level” abstractions.

Instead, consider that Profunctor gives us lmap.

lmap :: (y -> x) -> p x a -> p y a

The combination of lmap and (=?) (filter) is in fact equivalent to:

filterMap :: (y -> Maybe x) -> p x a -> p y a

The type of partial functions y -> Maybe x is essentially what the Invertible Syntax Descriptions paper uses to work with its own redefinition of Alternative (removing the Applicative superclass constraint), as one component of “partial isomorphisms”.

filterMap should satisfy some laws:

filterMap (f >=> g) = filterMap f . filterMap g
filterMap pure = id

So filterMap actually represents a functor; its domain is the Kleisli category associated with the Maybe monad.

Contravariant functors

We shall generalize Profunctor. Focusing on the first type parameter of p, we have that p must be a contravariant functor, from some arbitrary category associated with p, here called First p. In comparison, Profunctor specializes it to the Hask category of pure functions (First p = (->)).

The Category type class can be found in Control.Category, in base. The type syntax is allowed here by the TypeFamilies extension, allowing one to write type-level functions to some extent.

class Category (First p) => Contravariant p where
  type First p :: * -> * -> *
  lmap :: First p y x -> p x a -> p y a

Maybe

In the case of an applicative parser, its instance may use the Kleisli Maybe category to allow mappings to fail:

newtype Kleisli m y x = Kleisli (y -> m x)

instance Monad m => Category (Kleisli m)

instance Contravariant IParser where
  type First IParser = Kleisli Maybe
  lmap :: Kleisli Maybe y x -> IParser x a -> IParser y a
  lmap = (...)

A derived function can take care of unwrapping the Kleisli newtype in Haskell.

filterMap
  :: (Contravariant p, First p ~ Kleisli m, Monad m)
  => (y -> m x) -> p x a -> p y a
filterMap = lmap . Kleisli

Pure functions

Of course, there are profunctors which cannot fail, the obvious one being the function type (->).

instance Contravariant (->) where
  type First (->) = (->)
  lmap :: (y -> x) -> (x -> a) -> (y -> a)
  lmap f g = g . f

However, Contravariant may seem like too big of a generalization. In particular, we have lost the ability to map a pure function in general when the domain First p is not (->).

Arrows

We can use the fact that pure functions can still be lifted as Kleisli arrows. One fitting structure is arrows, as found in Control.Arrow, in base, it is situated somewhere between applicative functors and monads on the abstraction ladder, but we are more particularly interested in one method it provides: arr :: Arrow p => (y -> x) -> p y x.

(=.)
  :: (Contravariant p, Arrow (First p))
  => (y -> x) -> p x a -> p y a
(=.) = lmap . arr

There may be interesting non-arrow categories for bidirectional programming with profunctors, but I can’t think of any at the moment.

Using Kleisli arrows for the Maybe monad allows printers to fail for certain inputs. Monads are a very general notion, can we find uses for other effects?

State

One situation where we may need to perform side-effects with lmap is when the data we are working on is represented in some indirect way, e.g., with explicit pointers.

A more concrete example is hash consing: sharing values which are structurally equal. Deconstructing a hash-consed value may require a lookup in memory. Then we can imagine a hypothetical parser working in some hash consing monad H.

data HIParser x a

instance Contravariant HIParser where
  type First HIParser = Kleisli (MaybeT H)
  lmap :: Kleisli (MaybeT H) y x -> HIParser x a -> HIParser y a

Unrolling the type definitions, the type of lmap is equivalent to the following, with an arrow combining state and exception.

lmap :: (y -> H (Maybe x)) -> HIParser x a -> HIParser y a

I have written a more complete example of that in the new repository summarizing my current work as a Haskell package: profunctor-monad.

The bodies of two equivalent parsers are copied below, the first one with a monadic definition, the second one with a (primarily) applicative definition.

-- type p :: (* -> *) -> * -> * -> *
-- A monad transformer with parsing/printing functionality (via ``IParser``).
--
-- type M :: * -> *
-- A monad for hash consing and exceptions (for parse errors).
--
-- type P (p M) I = p M I I
ppTree
  :: forall p
  .  (Monad1 (p M), IParser (p M), First (p M) ~ Kleisli M, PMonadTrans p)
  => P (p M) I
ppTree = with @Monad @(p M) @TreeI $ uncons =: do
  c0 <- firstChar =. anyChar
  case c0 of
    '0' -> lift leaf
    '1' -> do
      i <- c1 =. ppTree
      j <- c2 =. ppTree
      lift (node i j)
    _ -> fail "Invalid character"
  where
    firstChar Leaf = '0'
    firstChar (Node _ _) = '1'
    c1 (Node i _) = i
    c2 (Node _ j) = j

ppTree2
  :: forall p
  .  ( Alternative1 (p M), Monad1 (p M), PMonadTrans p
     , IParser (p M), First (p M) ~ Kleisli M)
  => P (p M) I
ppTree2 =
  with @Alternative @(p M) @TreeI $
    uncons =:
      (   (guard . isLeaf) =: char '0' *> lift leaf
      <|> (guard . isNode) =: char '1' *> ppNode'
      )
  where
    ppNode' = with @Monad @(p M) @TreeI $ do
      i <- c1 =. ppTree2
      j <- c2 =. ppTree2
      lift (node i j)

    c1 (Node i _) = i
    c2 (Node _ j) = j

-- Maybe helpful definitions below.

-- Unique value identifier.
data I :: *

-- Shallow representation of a hash-consed tree.
data TreeI = Leaf | Node I I

-- Predicates.
isLeaf, isNode :: TreeF a -> Bool

-- Monadic smart constructors.
leaf :: H I
node :: I -> I -> H I