Note to myself: a theory of injective arrows
This post starts off from work that I have yet to publish so it won’t make any sense.
When programming bidirectionally with monadic profunctors, we assume that there
is a “round-tripping” property on values of type p u u, such that: if
m :: p u u is a round-trip, and f :: u -> p v v is such that f u is
a round-trip for all u, and f is an injective arrow with sagittal
inverse f' :: v -> Maybe u, then (f' =: m) >>= f is a round-trip.
In practice, free use of (>>=) and (=:) makes the code looks very
nice, but this leaves open the problem of ensuring that f is an
injective arrow, and that it is being used with its sagittal inverse f'.
The Invertible Syntax Descriptions paper has a similar problem with
partial isomorphisms, which they address with a small combinator library.
Can we do something similar here?
The focus of this post is thus combinators that preserve round-tripping properties entirely such that, if one starts with a set of primitive round-trips, and only uses these combinators, then the result must be a round-trip.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
module Injective.Arrows where
import Control.Applicative
import Control.Arrow (Arrow, Kleisli)
import Control.Monad
import Profunctor.MonadCombinators for injective arrows
A type for injective arrows and their inverses seems like a natural starting point. It shall be made opaque to users, whereas we liberally deconstruct it to define our combinators.
-- Most (all?) of the time, v ~ v'
data IArrow p u v v' = IArrow
{ arrow :: u -> p v v'
, inverse :: v -> Maybe u
}Then, instead of (f' =: m) >>= f, one should write m >>=+ f,
but now f carries its inverse at all times.
(>>=+)
:: (Cofunctor p, First p ~ Kleisli Maybe, Monad1 p)
=> p u u -> IArrow p u v v' -> p v v'
m >>=+ IArrow f f' = withMonad ((f' =: m) >>= f)Lifting Kleisli arrows
Any Kleisli arrow maps to an injective arrow by adding its input to the output.
liftArr
:: (Cofunctor p, First p ~ Kleisli Maybe, Functor1 p)
=> (u -> p v v') -> IArrow p u (u, v) (u, v')
liftArr k = IArrow
(\u -> withFunctor ((,) u <$> snd =. k u))
(\(u, _) -> return u)As a special case of this, noting that p v v is isomorphic to
() -> p v v:
constArr :: p v v' -> IArrow p () v v'
constArr m = IArrow (\() -> m) (\_ -> return ())This is useful to write the equivalent of (>>).
(>>+)
:: (Cofunctor p, First p ~ Kleisli Maybe, Monad1 p)
=> p () () -> p v v' -> p v v'
m >>+ n = m >>=+ constArr n
-- m >> n = m >>= \_ -> nCategory
Categories look nice. I can imagine (>>>) being useful.
iarrow0 :: forall p u. Monad1 p => IArrow p u u u
iarrow0 = IArrow
(\u -> withMonad (return u))
return
(>>>)
:: (Cofunctor p, First p ~ Kleisli Maybe, Monad1 p)
=> IArrow p u v v -> IArrow p v w' w -> IArrow p u w' w
IArrow a1 i1 >>> IArrow a2 i2 = IArrow
(\u -> withMonad ((i2 =: a1 u) >>= a2))
(i1 <=< i2)Constructors
This convenient pattern adapts applicative style to monadic profunctors.
(<.>)
:: (Cofunctor p, Arrow (First p), Applicative1 p)
=> p x a -> p y b -> p (x, y) (a, b)
mfst <.> msnd = withApplicative
((,) <$> fst =. mfst <*> snd =. msnd)This pattern for products can be generalized to other constructors with some generic programming.
Arrow-like
The Arrow interface gives some inspiration for a few more constructs
involving products.
second
:: (Cofunctor p, Arrow (First p), Functor1 p)
=> IArrow p u v v -> IArrow p (b, u) (b, v) (b, v)
second (IArrow a i) = IArrow
(\(b, u) -> withFunctor ((fmap ((,) b) . (=.) snd) (a u)))
(\(b, v) -> fmap ((,) b) (i v))
(***)
:: (Cofunctor p, Arrow (First p), Applicative1 p)
=> IArrow p u1 v1 v1 -> IArrow p u2 v2 v2
-> IArrow p (u1, u2) (v1, v2) (v1, v2)
IArrow a1 i1 *** IArrow a2 i2 = IArrow
(\(u1, u2) -> a1 u1 <.> a2 u2)
(\(v1, v2) -> liftA2 (,) (i1 v1) (i2 v2))
(&&&)
:: (Cofunctor p, Arrow (First p), Applicative1 p)
=> IArrow p u v1 v1 -> IArrow p u v2 v2
-> IArrow p u (v1, v2) (v1, v2)
IArrow a1 i1 &&& IArrow a2 _ = IArrow
(\u -> a1 u <.> a2 u)
(\(v1, _) -> i1 v1) -- Broken symmetryPattern matching
Dually to the above construct for product types, there is also one for
sums. Actually, there are two viable approaches. The one that
mirrors (<.>) best is to use Alternative.
(<||>)
:: (Cofunctor p, First p ~ Kleisli Maybe, Alternative1 p)
=> p x a -> p y b -> p (Either x y) (Either a b)
ma <||> mb = withAlternative
( Left <$> fromLeft =: ma
<|> Right <$> fromRight =: mb)
fromLeft :: Either a b -> Maybe a
fromLeft (Left x) = Just x ; fromLeft (Right _) = Nothing
fromRight :: Either a b -> Maybe b
fromRight (Right y) = Just y ; fromRight (Left _) = NothingOr we can pattern-match on an explicit parameter,
this looks like the dual to (***), i.e., (+++).
(<?>)
:: (Cofunctor p, First p ~ Kleisli Maybe, Functor1 p)
=> (a -> p x u) -> (b -> p y v)
-> (Either a b -> p (Either x y) (Either u v))
(mu <?> mv) ab = withFunctor $ case ab of
Left a -> Left <$> fromLeft =: mu a
Right b -> Right <$> fromRight =: mv bWe can wrap (<?>) for injective arrows.
(+++)
:: (Cofunctor p, First p ~ Kleisli Maybe, Functor1 p)
=> IArrow p a x u -> IArrow p b y v
-> IArrow p (Either a b) (Either x y) (Either u v)
IArrow a i +++ IArrow b j = IArrow
(a <?> b)
(\xy -> case xy of
Left x -> Left <$> i x
Right y -> Right <$> j y)Conclusion
These combinators work with generic sums and products with (,) and Either.
We need at least some way to restructure them to user-defined types.
Moreover, in this post I kept the profunctor presentation of bidirectional
programs, but in practice the input and output types will always be the same
in p u u.
We end up with something that overlaps greatly with Invertible Syntax
Descriptions, the main addition being a monadic extension with the
IArrow type.
This still feel unsatisfactory compared to the unbridled power of Monad,
but it is at odds with the strongest guarantees one may require in
some situations.
- How much expressiveness are we giving up?
- Are there more interesting and useful constructs?
- How can we improve the syntax when using these combinators?
I’m starting to think about a way to exploit Haskell’s
RebindableSyntaxextension in a very non-standard way, though it might lead nowhere.
I hope this will become clearer once I try to (re)write various bidirectional programs using them.