4
votes

I'm trying to capture a symmetrical data processing pipeline using arrows, and was wondering if bidirectional composition is possible.

Control.Arrow exposes the following

-- | Left to right composition
(>>>) :: Category cat => cat a b -> cat b c -> cat a c 

-- | Right to left composition
(<<<) :: Category cat => cat b c -> cat a b -> cat a c 

what I'd like, but cannot work out how to express is bidirectional composition of pairs. The type is something like.

(<^>) :: Category cat => cat (a,y) (b,z) -> cat (b,x) (c,y) -> cat (a,x) (c,z) 

where the first element of each pair is to composed left-to-right, and the second to be composed right-to-left.

2
I'm pretty sure this isn't possible, at least not without some audacious infinite knot tying or unsafe use of bottom values. It could very well be done under some extra constraints though – what's your actual use case?leftaroundabout
I've a configurable set of encoding / decoding operators, each of which has type (a,b) -> (a', b'). I can tie them together manually, but pipes seemed a good fit. It would be perfect, except the encoding pipeline needs to be composed left to light, and the decode right to left.OllieB
Why not just split the tuples and then compose?jkeuhlen
Maybe your are looking for something like Product (->) (Op (->)), exploiting Data.Category?chi
Basically, Op (->) is flipped (->), i.e. Op (->) a b is actually b -> a (function going "backward"). Then the category Product (->) (Op (->)) composes a morphism in (->) (forward function) and one in Op (->) (backward function). Hence, >>> in this category should work as you want.chi

2 Answers

5
votes

Here's an example of a category involving pairs of forward and backwards functions.

{-# LANGUAGE TypeOperators, GADTs #-}

import Prelude hiding ((.))
import Data.Category
import Data.Category.Product

type C = (->) :**: (Op (->))

The above states that C (a,b) (c,d) is isomorphic to a pair (a->c, d->b). Pairs "compose" in the category in the natural way: the forward functions are composed forwards, the backwards functions are composed backwards.

Here are two examples:

f :: C (String, Bool) (Int, Char)
f = length :**: Op (=='a')

Note how the backwards function has to be wrapped in an Op (belongs to the "opposite" category).

g :: C (Int, Char) ([Int], Maybe Char)
g = (\x->[x,x]) :**: Op (maybe 'X' id)

Note how the "source" of g is the "target" of f. This ensures composition is possible.

composed :: C (String, Bool) ([Int], Maybe Char)
composed = g . f

test :: ([Int], Bool)
test = case composed of
   (forward :**: Op backward) -> (forward "abcde", backward Nothing)
-- result: ([5,5],False)

On a more practical side, note that Data.Category and Control.Category are different beasts :-( and that the Control.Arrow library mentioned in the question uses the latter.

Still, it should be possible to define Op and :**: for Control.Category as well. Maybe it's already on hackage somewhere (?).

2
votes

Some further approaches, best recorded as a separate answer.

The first imposes the additional constraint of an ArrowLoop, and is defined using a recursive arrow do notation.

From a data flow viewpoint however, no recursion is taking place.

(<->) ∷ (ArrowLoop a) ⇒ a (b,f) (c,g) → a (c,e) (d,f) → a (b,e) (d,g)
(<->) f1 f2 = proc (b, e) → do
  rec
    (c,g) ← f1 ↢ (b,f)
    (d,f) ← f2 ↢ (c,e)
  returnA ↢ (d,g)

It could equally be defined as

(<->) ∷ (ArrowLoop a) ⇒ a (b,f) (c,g) → a (c,e) (d,f) → a (b,e) (d,g)
(<->) f1 f2 = proc (b, e) → do
  rec
    (d,f) ← f2 ↢ (c,e)
    (c,g) ← f1 ↢ (b,f)
  returnA ↢ (d,g)

The second approach does not: I've yet to work out if this is a sane thing to do.

(<->) ∷ (Arrow a) ⇒ a (b,f) (c,g) → a (c,e) (d,f) → a (b,e) (d,g)
(<->) f1 f2 = proc (b, e) → do
  (c,_) ← f1 ↢ (b,undefined)
  (d,_) ← f2 ↢ (c,undefined)
  (_,f) ← f2 ↢ (undefined,e)
  (_,g) ← f1 ↢ (undefined,f)
  returnA ↢ (d,g)

The following is the same as the second approach, but defined explicitly in terms of composition functions.

(<->) ∷ (Arrow a) ⇒ a (b,f) (c,g) → a (c,e) (d,f) → a (b,e) (d,g)
(<->) f g =
  let toFst x = (x,undefined)
      toSnd x = (undefined,x)
  in
    (arr toFst ⋙ f ⋙ arr fst ⋙ arr toFst ⋙ g ⋙ arr fst) ⁂
    (arr snd ⋘ f ⋘ arr toSnd ⋘ arr snd ⋘ g ⋘ arr toSnd)