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
checkValid v = studentCount v > 0
instance OnOneParameter SportsTeam where
checkValid v = memberCount v > 0 && memberCount v < 10This can be easily tested using the below snippet
mainp :: IO ()
main = do
let class_room = ClassRoom {className="Math", studentCount=10}
sports_team = SportsTeam {teamName="L", memberCount=6}
putStrLn $ show $ checkValid class_room
putStrLn $ show $ checkValid sports_teamOverloading 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
checkValidTogether a b = checkValid a && checkValid b
instance (OnOneParameter b) => OnTwoParameters SportsTeam b where
checkValidTogether a b = checkValid a && checkValid b
instance (OnOneParameter b) => OnTwoParameters ClassRoom b where
checkValidTogether a b = checkValid a && checkValid b
checkValid2 :: SportsTeam -> SportsTeam -> Bool
checkValid2 a b = checkValidTogether a bOne example which is ambiguous is:
testFn :: (OnOneParameter b) => ClassRoom -> b -> Bool
testFn a b = checkValidTogether a bHere 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
arising from a use of ‘checkValidTogether’
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:10Overloading 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
iAmCalledBasedOnReturnType _ = True
instance OnReturnType Int where
iAmCalledBasedOnReturnType _ = 10Here 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
iAmCalledBasedOnReturnTypeAndParams a = if a == 1 then True else
False
instance OnReturnTypeAndParams Bool Int where
iAmCalledBasedOnReturnTypeAndParams a = if a then 1 else 0Here 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
nestedType c = show c