Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generalising class parameters

Lets say I've got the class:

class C a b t where
  f :: (a, b) -> (t a, t b)

Now with this definition, I can define instances for say:

(a,b) -> (Maybe a, Maybe b)
(a,b) -> ([a], [b])

But not for (as far as I understand):

(a,b) -> (a,b)
(a,b) -> ((a, a), (b, b))

I could instead change my class definition like so:

type family T a b x

class C a b where
  f :: (a, b) -> (T a b a, T a b b)

Which would allow me to do the above, but then I'd only be able to declare one f for each a and b.

Basically I want to be able to pass a type family as t in the original definition, which is implicitly resolved by the type checker if known. I don't want to just make it f :: (a, b) -> (c, d) as I want to maintain the invariant that both a and b have the same thing done to them, so swap . f is the same type as f . swap. I'm thinking I might need injective type families (from GHC 8.0) but I'm not sure. But maybe there's another way?

like image 492
Clinton Avatar asked Sep 23 '15 06:09

Clinton


1 Answers

This may or may not answer your question depending on what you actually need to do.

A standard solution is to use a newtype to define new instances. E.g.

newtype Pair a = Pair (a, a)
instance C a b Pair where
   f = ...

However, this will cause f to have type

f :: (a, b) -> (Pair a, Pair b)

instead of the wanted

f :: (a, b) -> ((a, a), (b, b))

The latter can be recovered in a boring way:

f' :: (a, b) -> ((a, a), (b, b))
f' p = case f p of (Pair x, Pair y) -> (x, y)

Now, writing "adapter" functions such as f' feels redundant: after all we are simply removing a newtype wrapper, which does not change the runtime representation. Worse, this could be inefficient: consider transforming [Pair a] into [(a, a)]. To do that, we would need to map over the whole list, so we pay O(n) do do essentially nothing.

An efficient alternative can be constructed using safe coercions:

import Data.Coerce

f'' :: forall a b. (a, b) -> ((a, a), (b, b))
f'' = coerce (f :: (a, b) -> (Pair a, Pair b))

This allows the use of newtypes to drive instance selection, and yet remove them when they get in the way.

Now, if your aim really is to define new instances without newtypes, this does not help much. If instead you want a simple way to remove newtypes wrappers from instance methods, it can help.

like image 64
chi Avatar answered Oct 13 '22 17:10

chi