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
.
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.)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With