Polymorphism in Haskell
In a dynamic language like Python, there is no strict concept of function overloading. Though, one can overload operators, function overloading(other than operators) just means that you shadow the newest instance of a function (that is functions sharing the same
name) over the old instance.
Function over-loading is also referred to as ad-hoc polymorphism. Haskell uses type classes
to provide ad-hoc polymorphism.
Ad-hoc polymorphism by way of type classes
is much powerful when compared to function overloading features you get in other static languages like Java. For a language to support function overloading, it is required that the runtime know the type of arguments and return types that is inferred at the call site. This knowledge of types is used in dispatching the call to the appropriate overloaded function. Dispatching to methods can be done based on
arguments
return type
a combination of both.
Not all languages let you do dispatching based on all the above(for example, in Java, dispatching to overloaded functions is only based on argument types and not return type. Haskell can do a dispatch to an function based on the return type as well.
What follows are some example of ad-hoc polymorphism in use. We will assume we are working with following data types:
data ClassRoom = ClassRoom {className::String, studentCount::Int}
data SportsTeam = SportsTeam {teamName::String, memberCount::Int}
Overloading on one argument of a function.
Say we have the two record types shown above, and we want an overloadeded function called checkValid
to work with both types.
We can use type classes
to implement the overloaded functions
-- Wrapped the checkValud function in a type class
class OnOneParameter a where
-- One one parameter
checkValid :: a -> Bool
instance OnOneParameter ClassRoom where
= studentCount v > 0
checkValid v
instance OnOneParameter SportsTeam where
= memberCount v > 0 && memberCount v < 10 checkValid v
This can be easily tested using the below snippet
mainp :: IO ()
= do
main let class_room = ClassRoom {className="Math", studentCount=10}
= SportsTeam {teamName="L", memberCount=6}
sports_team putStrLn $ show $ checkValid class_room
putStrLn $ show $ checkValid sports_team
Overloading on more than one argument.
If we want to over-load the function with more than one argument, then we will also have to use MultiParamTypeClasses
extension. Using this extensions, we get the power to dispatch on multiple arguments.
We see that the class definition has two parameters and we can define three different instances of the checkValidTogether
method. Each of these instances will be called based on the types of both the arguments at the call site.
-- On 2 parameters
class OnTwoParameters a b where
checkValidTogether :: a -> b -> Bool
instance OnTwoParameters ClassRoom SportsTeam where
= checkValid a && checkValid b
checkValidTogether a b
instance (OnOneParameter b) => OnTwoParameters SportsTeam b where
= checkValid a && checkValid b
checkValidTogether a b
instance (OnOneParameter b) => OnTwoParameters ClassRoom b where
= checkValid a && checkValid b
checkValidTogether a b
checkValid2 :: SportsTeam -> SportsTeam -> Bool
= checkValidTogether a b checkValid2 a b
One example which is ambiguous is:
testFn :: (OnOneParameter b) => ClassRoom -> b -> Bool
= checkValidTogether a b testFn a b
Here the compiler, does not know which version of the overloaded function should the call be dispatched to. Compiling, the above snippet will cause the compile to fail with this error:
Overlapping instance error
Overlapping instances for OnTwoParameters ClassRoom b
of ‘checkValidTogether’
arising from a use Matching instances:
instance OnOneParameter b => OnTwoParameters ClassRoom b
Defined at /tmp/flycheck10671M3X/Polymorphism.hs:40:10
instance OnTwoParameters ClassRoom SportsTeam
Defined at /tmp/flycheck10671M3X/Polymorphism.hs:34:10
Overloading on return type
Here, the function arguments could be the same, but notice that the dispatch is happening on the return type of the function
class OnReturnType a where
iAmCalledBasedOnReturnType :: Int -> a
instance OnReturnType Bool where
= True
iAmCalledBasedOnReturnType _
instance OnReturnType Int where
= 10 iAmCalledBasedOnReturnType _
Here is the output, from calling the above functions.
> iAmCalledBasedOnReturnType 10::Int
10
> iAmCalledBasedOnReturnType 10::Bool
True
>
Overloading functions on both argument types and return type
This example just builds on the examples we have seen above.
class OnReturnTypeAndParams a b where
iAmCalledBasedOnReturnTypeAndParams :: a -> b
instance OnReturnTypeAndParams Int Bool where
= if a == 1 then True else
iAmCalledBasedOnReturnTypeAndParams a False
instance OnReturnTypeAndParams Bool Int where
= if a then 1 else 0 iAmCalledBasedOnReturnTypeAndParams a
Here is the output of using the above code
>
> let x_int = 10::Int
> iAmCalledBasedOnReturnTypeAndParams x_int::Bool
False
> let y_bool = True
> iAmCalledBasedOnReturnTypeAndParams y_bool::Int
1
>
By now, we see that Haskell provides constructs that lets us build a powerful set of abstractions using ad-hoc polmorphism.
As a bonus, here is an example of ad-hoc polymorphism for nested types:
class OnNestedType a where
nestedType :: a -> String
-- Examples of nested types
instance (Show a) => OnNestedType [a] where
= show c nestedType c