Modeling monoidal coherence in Haskell
Asked Answered
H

0

6

Oftentimes, whenever working with monoidal operators in Haskell (or in any other languages for that matter), I find myself asking "Wouldn't it be convenient if the language itself had support for the normalization of monoidal operations? Allowing us to work in something like a strict monoidal category?"

Recently, I had the idea that something similar to this (albeit, not exactly the same, as e.x. ((), a) and a will still be distinct types could be implemented in Haskell by using type families. To see what I mean, consider the following types:

-- From Control.Arrow
(***) :: a b c -> a b' c' -> a (b, b') (c, c')

x :: MyArrow () Int
y :: MyArrow String Int

Currently with this API, combining x and y with *** yields:

x *** y :: MyArrow ((),String) (Int,Int)

But from a "monoidal" perspective the type I would really want to see would be the isomorphic:

x *** y :: MyArrow String (Int,Int)

Whereas, to actually achieve this using the standard arrow combinators, one would need to write:

(\x -> ((), x)) ^>> (x *** y) :: MyArrow String (Int,Int)

Now, to get around this boilerplate (assuming we don't want to deal with this kind of book-keeping for simplifying monoidal types to a normal form), my idea was the use the type family:

type family Reduce (pi :: * -> * -> *) (u :: *) (x :: *) :: * where
  Reduce pi u (pi u x) = Reduce pi u x
  Reduce pi u (pi x u) = Reduce pi u x
  Reduce pi u (pi (pi x y) z) = Reduce pi u (pi 
    (Reduce pi u x) 
    (pi 
       (Reduce pi u y) 
       (Reduce pi u z)
    )
   )
  Reduce pi u x = x

Trying this out at the ghci repl, this seems to give me the correct "normalized" monoidal types:

> let x :: Reduce (,) () (((), Int), String) = undefined
> :t x
x :: (Int, [Char])

Great! So, the idea would be to re-write any combinator introducing a monoidal operation so that it applies the Reduce type family:

-- My version
(***) :: a b c -> a b' c' -> a (Reduce (,) () (b, b')) (Reduce (,) () (c, c'))

x :: MyArrow () Int
y :: MyArrow String Int
x *** y :: MyArrow String (Int, Int)

The issue now being, how do I write my version of (***)? I would need to do something like "pattern matching" on types. That's no good. So, next I tried to use type classes with associated types instead.

class Reducible (pi :: * -> * -> *) (u :: *) (x :: *) | pi -> u where
  type Reduce x :: *
  reduce :: x -> Reduce x
  unReduce :: Reduce x -> x

Alright, so now we have an associated Reduce type to apply the monoidal normalization to our types, but also some operations to actually write functions to and from the reduced reresentations. Next, I wrote a utility Monoidal typeclass supplying the monoidal coherence isomorphisms to facilitate implementing the Reducible typeclass:

class Monoidal pi u where
  lambdaR :: pi u x -> x
  lambdaRi :: x -> pi u x
  lambdaL :: pi x u -> x
  lambdaLi :: x -> pi x u
  assocR :: pi x (pi y z) -> pi (pi x y) z
  assocL :: pi (pi x y) z -> pi x (pi y z)

Now here is where the problem comes along. I define the first two instances corresponding to the cases for reducing monoidal units...

instance (Monoidal pi u, Reducible pi u x) => Reducible pi u (pi u x) where
  type Reduce (pi u x) = x
  reduce x = lambdaR x
  unReduce x = lambdaRi x

instance (Monoidal pi u, Reducible pi u x) => Reducible pi u (pi x u) where
  type Reduce (pi x u) = x
  reduce x = lambdaL x
  unReduce x = lambdaLi x

...and GHC tells me:

main.hs:18:8: error:
    Conflicting family instance declarations:
      Reduce (pi u x) = x -- Defined at main.hs:18:8
      Reduce (pi x u) = x -- Defined at main.hs:23:8
   |
18 |   type Reduce (pi u x) = x
   |  

Now, the issue here seems to be the distinction between open and closed type families. As I understand it, associated types act like open type families, where what I had written before was a closed type family, and open type families have overlap restrictions (much like type classes).

I found an unresolved GHC issue which I think might be relevant here, but beyond that (as it is unclear if/when this will be implemented) -- are there any workarounds to this issue that will let me define my Reduce typeclass with reduce and unReduce functions in a released GHC version?

Hester answered 15/5, 2021 at 17:33 Comment(7)
Yes, this problem has often bugged me too. I'm afraid there is no 100% satisfying solution. This would really need built-in support from the language – which would be a big research topic with all kinds of complicated ramifications upon type checking and pattern matching.Aldershot
...though, actually the particular problem with your two instances would go away if you just hard-coded the case u ~ (), no?Aldershot
Could you make you associated type family take the same number of arguments as the typeclass, and then ignore the arguments you don't use? Even if you ignore them, the extra arguments might help avoid conflicts.Trouble
@Aldershot It does look like hard-coding the case of u ~ () makes GHC happy. Thanks!Hester
@Trouble That appears to help as well.Hester
Another idea is to keep Reduce as a closed type family. On other words, when you made the Reducible type class, why not reference the original Reduce type family instead of turning Reduce into an associated type?Lambent
I have been able to solve most of the problems allowing myself to use unsafeCoerce. The code is here (gist.github.com/mroman42/b63ea565227bdbac14b063088d79ad34). Each particular case separately works perfectly, but I have not been able to make a general coherence function work. Would anyone know why my coherence there does not work?Christos

© 2022 - 2024 — McMap. All rights reserved.