Polymorphism in Haskell

2016-09-12

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 < 10

This 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_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
      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 b

One example which is ambiguous is:


    testFn :: (OnOneParameter b) => ClassRoom -> b -> Bool
    testFn a b = checkValidTogether 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
       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: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
      iAmCalledBasedOnReturnType _ = True

    instance OnReturnType Int where
      iAmCalledBasedOnReturnType _ = 10

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
      iAmCalledBasedOnReturnTypeAndParams a = if a == 1 then True else
      False

    instance OnReturnTypeAndParams Bool Int where
      iAmCalledBasedOnReturnTypeAndParams a = if a then 1 else 0

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
              nestedType c = show c