I want to create two typeclasses, A
and B
, where A
is a superclass of B
. The functions defined in B
are sufficient to implement those in A
. Then, if I have a function with the constraint fun :: (A thing) => ...
an instance of B
for, say, Int
, I'd like to be able to pass an Int
to fun
without creating a duplicate instance A
for Int
.
For example, let's say I have a type class which can check if value is "even". Then, I have another type class which can check if a value is divisible by some number. The second type class is powerful enough to implement the functions in the first, and any function which only requires "even-checking" capabilities should be able to accept an argument which has "divisible-by" abilities.
Here's what I think it would look like:
class IsEven a where
isEven :: a -> Bool
class (IsEven a) => DivisibleBy a where
divisibleBy :: a -> Int -> Bool
isEven :: a -> Bool
isEven a = divisibleBy a 2
printIsEven :: (IsEven a) => a -> IO ()
printIsEven a = putStrLn (show (IsEven.isEven a))
instance IsEven Int -- I need to do this or I cannot create a DivisibleBy instance
instance DivisibleBy Int where
divisibleBy a i = a `mod` i == 0
myint :: Int
myint = 2
main :: IO ()
main = printIsEven myint
However, at compile time this produces the warning:
[2 of 2] Compiling Main ( Foo.hs, Foo.o )
Foo.hs:11:10: warning: [-Wmissing-methods]
• No explicit implementation for
‘IsEven.isEven’
• In the instance declaration for ‘IsEven Int’
|
11 | instance IsEven Int
| ^^^^^^^^^^
Linking Foo ...
and at runtime, the program fails:
Foo: Foo.hs:11:10-19: No instance nor default method for class operation isEven
How can I achieve this subtyping effect without duplicating logic into an instance IsEven
?
As far as I know, the closest you can get in standard Haskell is
instance IsEven Int where
isEven n = n `divisibleBy` 2
instance DivisibleBy Int where
divisibleBy a i = a `mod` i == 0
You don't have to duplicate the logic (indeed, you can implement isEven
in terms of divisibleBy
), but you still need to provide an explicit definition.
You would have to repeat this pattern for every type you want to make an instance of DivisibleBy
.
Using the DefaultSignatures
language extension you can also do the following:
{-# LANGUAGE DefaultSignatures #-}
class IsEven a where
isEven :: a -> Bool
default isEven :: (DivisibleBy a) => a -> Bool
isEven n = n `divisibleBy` 2
class (IsEven a) => DivisibleBy a where
divisibleBy :: a -> Int -> Bool
instance IsEven Int
instance DivisibleBy Int where
divisibleBy a i = a `mod` i == 0
This moves the default implementation to the class itself. Now you can indeed just say instance IsEven Int
without providing an instance body. The disadvantage is that now IsEven
has to know about DivisibleBy
, and you can only provide one default
implementation.
You can't redefine a method in a new class and have it affect the one in the old class. If you want methods to work like this, the parent class has to reference the child class.
You need the DefaultSignatures
extension to make this work. Turn it on and then change your classes to this:
class IsEven a where
isEven :: a -> Bool
default isEven :: DivisibleBy a => a -> Bool
isEven a = divisibleBy a 2
class IsEven a => DivisibleBy a where
divisibleBy :: a -> Int -> Bool
With GHC 8.6 and above, this can also be achieved through DerivingVia
:
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
{-# LANGUAGE StandaloneDeriving #-}
-- Class definitions:
class IsEven a where
isEven :: a -> Bool
-- Note that we don't need to have IsEven as a superclass.
class DivisibleBy a where
divisibleBy :: a -> Int -> Bool
-- Boilerplate that only needs to be written once:
-- Boilerplate DivisibleBy instance generated with GeneralisedNewtypeDeriving.
newtype WrappedDivisibleBy a = WrapDivisibleBy { unwrapDivisibleBy :: a }
deriving DivisibleBy
instance DivisibleBy a => IsEven (WrappedDivisibleBy a) where
isEven n = n `divisibleBy` 2
-- Instance example:
instance DivisibleBy Int where
divisibleBy a i = a `mod` i == 0
-- Boilerplate IsEven instance generated with DerivingVia
-- (and StandaloneDeriving, as we aren't defining Int here).
deriving via (WrappedDivisibleBy Int) instance IsEven Int
DerivingVia
is not always an option (in the case of classes like Traversable
, which have an extra type constructor wrapping things in the type signature, it clashes with the role system); when it works, though, it is very neat.
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