This is a question about multiple dispatch in Haskell.
Below I use the term \"compliant to [type class]\" to mean \"has type which is instance of [type class]\", because
Like Christian Conkle hinted at, we can determine if a type has an Integral
or Floating
instance using more advanced type system features. We will try to determine if the second argument has an Integral
instance. Along the way we will use a host of language extensions, and still fall a bit short of our goal. I'll introduce the following language extensions where they are used
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverlappingInstances #-}
To begin with we will make a class that will try to capture information from the context of a type (whether there's an Integral
instance) and convert it into a type which we can match on. This requires the FunctionalDependencies
extension to say that the flag
can be uniquely determined from the type a
. It also requires MultiParamTypeClasses
.
class IsIntegral a flag | a -> flag
We'll make two types to use for the flag
type to represent when a type does (HTrue
) or doesn't (HFalse
) have an Integral
instance. This uses the EmptyDataDecls
extension.
data HTrue
data HFalse
We'll provide a default - when there isn't an IsIntegral
instance for a
that forces flag
to be something other than HFalse
we provide an instance that says it's HFalse
. This requires the TypeFamilies
, FlexibleInstances
, and UndecidableInstances
extensions.
instance (flag ~ HFalse) => IsIntegral a flag
What we'd really like to do is say that every a
with an Integral a
instance has an IsIntegral a HTrue
instance. Unfortunately, if we add an instance (Integral a) => IsIntegral a HTrue
instance we will be in the same situation Christian described. This second instance will be used by preference, and when the Integral
constraint is encountered it will be added to the context with no backtracking. Instead we will need to list all the Integral
types ourselves. This is where we fall short of our goal. (I'm skipping the base Integral
types from System.Posix.Types
since they aren't defined equally on all platforms).
import Data.Int
import Data.Word
import Foreign.C.Types
import Foreign.Ptr
instance IsIntegral Int HTrue
instance IsIntegral Int8 HTrue
instance IsIntegral Int16 HTrue
instance IsIntegral Int32 HTrue
instance IsIntegral Int64 HTrue
instance IsIntegral Integer HTrue
instance IsIntegral Word HTrue
instance IsIntegral Word8 HTrue
instance IsIntegral Word16 HTrue
instance IsIntegral Word32 HTrue
instance IsIntegral Word64 HTrue
instance IsIntegral CUIntMax HTrue
instance IsIntegral CIntMax HTrue
instance IsIntegral CUIntPtr HTrue
instance IsIntegral CIntPtr HTrue
instance IsIntegral CSigAtomic HTrue
instance IsIntegral CWchar HTrue
instance IsIntegral CSize HTrue
instance IsIntegral CPtrdiff HTrue
instance IsIntegral CULLong HTrue
instance IsIntegral CLLong HTrue
instance IsIntegral CULong HTrue
instance IsIntegral CLong HTrue
instance IsIntegral CUInt HTrue
instance IsIntegral CInt HTrue
instance IsIntegral CUShort HTrue
instance IsIntegral CShort HTrue
instance IsIntegral CUChar HTrue
instance IsIntegral CSChar HTrue
instance IsIntegral CChar HTrue
instance IsIntegral IntPtr HTrue
instance IsIntegral WordPtr HTrue
Our end goal is to be able to provide appropriate instances for the following class
class (Num a, Num b) => Power a b where
pow :: a -> b -> a
We want to match on types to choose which code to use. We'll make a class with an extra type to hold the flag for whether b
is an Integral
type. The extra argument to pow'
lets type inference choose the correct pow'
to use.
class (Num a, Num b) => Power' flag a b where
pow' :: flag -> a -> b -> a
Now we'll write two instances, one for when b
is Integral
and one for when it isn't. When b
isn't Integral
, we can only provide an instance when a
and b
are the same.
instance (Num a, Integral b) => Power' HTrue a b where
pow' _ = (^)
instance (Floating a, a ~ b) => Power' HFalse a b where
pow' _ = (**)
Now, whenever we can determine if b
is Integral
with IsIntegral
and can provide a Power'
instance for that result, we can provide the Power
instance which was our goal. This requires the ScopedTypeVariables
extension to get the correct type for the extra argument to pow'
instance (IsIntegral b flag, Power' flag a b) => Power a b where
pow = pow' (undefined::flag)
Actually using these definitions requires the OverlappingInstances
extension.
main = do
print (pow 7 (7 :: Int))
print (pow 8.3 (7 :: Int))
print (pow 1.2 (1.2 :: Double))
print (pow 7 (7 :: Double))
You can read another explanation of how to use FunctionalDependencies
or TypeFamilies
to avoid overlap in overlapping instances in the Advanced Overlap article on HaskellWiki.