Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to "extend" classes in Haskell

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?

like image 627
David Abrahams Avatar asked Jul 06 '19 04:07

David Abrahams


3 Answers

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.

like image 189
melpomene Avatar answered Oct 19 '22 13:10

melpomene


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
like image 34
Joseph Sible-Reinstate Monica Avatar answered Oct 19 '22 14:10

Joseph Sible-Reinstate Monica


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.

like image 5
duplode Avatar answered Oct 19 '22 13:10

duplode