Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defining partially applied typeclasses

Exploring the idea that typeclasses are essentially C++ abstract classes without nested inheritance, I have written the typeclass

class Interface i c where
    i :: c -> i

instance Interface i i where i = id

infixl 1 #
(#) :: Interface i c => c -> (i -> r) -> r
c # f = f $ i c

With an interface like

data IDrawable' = IDrawable { draw :: IO () }

I'd like to have something like

type IDrawable c = Interface IDrawable' c

So that I could do

data Object = Object { objectDraw :: IO () }
data Person = Person { personDraw :: IO () }

instance IDrawable Object where i = IDrawable . objectDraw
instance IDrawable Person where i = IDrawable . personDraw

While the type IDrawable c compiles with ConstraintKinds, I'm not allowed to do instance IDrawable Object where i = IDrawable . objectDraw with the error

'i' is not a (visible) method of class 'IDrawable`

Is there a way to declare IDrawable c = Interface IDrawable' c so that it can be instanced?

This is purely out of academic interest, I'm not recommending anyone use this pattern in a real application, I just want to know if this sort of thing is possible without applying TemplateHaskell or CPP.

like image 212
bheklilr Avatar asked Mar 13 '15 18:03

bheklilr


2 Answers

No, this isn't possible (as of 7.8.3, and I think also 7.10); it's GHC bug #7543. It's not a very trafficked bug; there are clearly at least a few people who'd like to be able to write this sort of thing (e.g., you, Edward Kmett), but mostly this goes unnoticed. There's no progress on changing this behavior recorded on the tracker.

As for why you can't, let me paraphrase Simon Peyton-Jones's explanation on the bug tracker. The problem is that type checking instances has two parts: first, GHC has to look up where the method names (here, i) are from; second, GHC has to expand the type synonyms. Because these two steps are performed in this issue by two different components of GHC, constraint synonym instances can't be supported; GHC can't tell what class it needs to look in to find i.


The other reason this is a bug – and the reason I found this, as per the comments on András Kovács's answer – is that the current behavior isn't as simple as "it doesn't work". Instead, it tries to work, but you can't declare any methods… but you can declare a methodless instance! In GHCi:

GHCi, version 7.8.3: http://www.haskell.org/ghc/  :? for help
...
Prelude> :set -XMultiParamTypeClasses -XFlexibleInstances -XConstraintKinds
Prelude> class Interface i c where i :: c -> i
Prelude> instance Interface i i where i = id
Prelude> let (#) :: Interface i c => c -> (i -> r) -> r ; c # f = f $ i c ; infixl 1 #
Prelude> data IDrawable' = IDrawable { draw :: IO () }
Prelude> type IDrawable = Interface IDrawable'
Prelude> instance IDrawable () where i _ = IDrawable $ return ()

<interactive>:8:29:
    ‘i’ is not a (visible) method of class ‘IDrawable’
Prelude> ()#draw

<interactive>:9:3:
    No instance for (Interface IDrawable' ()) arising from a use of ‘#’
    In the expression: () # draw
    In an equation for ‘it’: it = () # draw
Prelude> instance IDrawable () where {}

<interactive>:10:10: Warning:
    No explicit implementation for
      ‘i’
    In the instance declaration for ‘Interface IDrawable' ()’
Prelude> ()#draw
*** Exception: <interactive>:10:10-21: No instance nor default method for class operation Ghci1.i

In other words:

instance IDrawable () where i _ = IDrawable $ return ()

fails, but

instance IDrawable () where {}

succeeds! So clearly, the check needs to either be loosened or tightened, depending on the desired behavior :-)


P.S.: One more thing: You should always eta-reduce type synonyms as much as possible. This is why I changed IDrawable to

type IDrawable = Interface IDrawable'

and dropped the c parameter on both sides in the GHCi code above. The advantage of this is that since type synonyms can't be partially applied, you can't pass your version of IDrawable as a parameter to anything; however, the fully eta-reduced version can be passed anywhere expecting something of kind * -> Constraint.

(This is touched on in András Kovács's answer, and I mentioned this in a comment there; nevertheless, since I ended up writing an answer too, I figured I'd add it here as well.)

like image 162
Antal Spector-Zabusky Avatar answered Oct 21 '22 07:10

Antal Spector-Zabusky


You can declare a "partial" class with no instances:

class Interface IDrawable' c => IDrawable c

instance Interface IDrawable' Object where i = IDrawable . objectDraw
instance Interface IDrawable' Person where i = IDrawable . personDraw

Alternatively, constraint synonyms could be used:

type IDrawable c = Interface IDrawable' c

The classy solution is probably preferable though, since the IDrawable class has a proper * -> Constraint kind, while the type synonym is unusable unless fully applied. This can be relevant, since data definitions (and type families and pretty much all type-level hackery) can only make use of proper type constructors.

like image 1
András Kovács Avatar answered Oct 21 '22 05:10

András Kovács