2

Given the following Haskell code snapshot:

class Foo a where
   bar  :: a -> ...
   quux :: a -> ...
   ...

Where the value of a is determined at runtime - the class dispatches on this value.

I'm assuming that the compiler can statically check the types at compile-time, and ensure that no invalid types can dispatch on it.

Now if we compare this to a dynamic dispatch in Java:

interface Flippable {
  Flippable flip();
}

class Left implements Flippable {
  Right flip();
}

class Right implements Flippable {
  Left flip();
}

class Demo {
  public static void main(String args[]) {
    Flippable flippable = new Right();
    System.out.println(flippable.flip);
  }
}

Assumptions:

  • Haskell can dispatch on return type as well as multiple arguments making dispatch different to other languages.

My question is: Is the dispatch of a Haskell TypeClass dynamic?

hawkeye
  • 34,745
  • 30
  • 150
  • 304

2 Answers2

7

If your code is Haskell 2010, with no language extensions turned on, Haskell actually doesn't support run-time polymorphism at all!

That's quite surprising. Haskell feels like a very polymorphic language. But in fact, all the types can in principle be decided purely at compile-time. (GHC chooses not to, but that's an implementation detail.)

This is exactly the same situation as C++ templates. When you write something like std::vector<int>, the compiler knows, at compile-time, all the times involved. The really surprising thing is just how rarely you actually need true run-time polymorphism!

Now, there are scenarios where you want to run different code based on run-time circumstances. But in Haskell, you can do that just by passing a function as an argument; you don't need to create a "class" (in the OOP sense) merely to achieve this.

Now, if you turn on some language extensions (most conspicuously ExistentialQuantification) then you get true, run-time polymorphism.

Note that the main reason people seem to do this is so you can do

class Foo f where
  bar :: f -> Int
  baz :: f -> Bool

data AnyFoo = forall f. Foo => AnyFoo f

my_list :: [AnyFoo]
...

This is widely considered a Haskell anti-pattern. In particular, if you upcast stuff in Java to put it into a list, you then later downcast it again. But the code above offers no possibility to ever downcast. You also can't use run-time reflection (since Haskell doesn't have that either). So really, if you have a list of AnyFoo, the only thing you can do with it is call foo or bar on it. So... why not just store the result of foo and bar?

data AnyFoo = AnyFoo {foo :: Int, bar :: Bool}

It lets you do exactly the same stuff, but doesn't require any non-standard extensions. In fact, in some ways, it's actually a bit more flexible. You now don't even need a Foo class, you don't need to define a new type for every sort of Foo you might have, just a function that constructs the AnyFoo data structure for it.

MathematicalOrchid
  • 61,854
  • 19
  • 123
  • 220
  • 2
    doesn't `Typeable` gives possibility to "downcast" (not saying it's a good idea though). – phadej Jan 27 '15 at 12:39
  • @phadej Quite right. The pattern I showed above does _not_ allow downcasting. But `Typeable` and `Dynamic` exist precisely to allow downcasting. – MathematicalOrchid Jan 27 '15 at 12:41
  • 6
    It's not true that Haskell 2010 doesn't require runtime polymorphism. Polymorphic recursion is runtime polymorphism, and part of the Haskell 2010 standard. `f :: Show a => a -> Int -> String ; f x 0 = show x ; f x n = f (x,x) (n - 1)`, for instance. – Carl Jan 27 '15 at 13:43
  • @Carl That's interesting. This is the first example of polymorphic recursion I've seen that doesn't actually admit a static resolution. It still seems a bit contrived though; I can't imaging much real-world code does this... – MathematicalOrchid Jan 27 '15 at 13:56
  • 3
    Not much, but occasionally it happens. Some of the algorithms working over `Data.Sequence.Seq` involve polymorphic recursion, due to the definition of the structure. – Carl Jan 27 '15 at 14:17
  • "if you have a list of AnyFoo, the only thing you can do with it is call foo or bar on it. So... why not just store the result of foo and bar?" --> Because foo and bar have side effects! In Java UI, it is very common to put objects into List and repeatedly take those objects out and ask them to .draw(), never downcasting them again. – David Jeske May 05 '17 at 00:22
  • In Haskell, expressions cannot cause side effects. That's why MathematicalOrchid said specifically that it's a Haskell anti-pattern. – portalguy15837 Jul 19 '21 at 08:12
2

It depends what you mean by "dynamic" dispatch. There aren't subtyping in haskell, so your Java example is hard to translate.

In situation when you have type class, say Show and want to put different elements into the list, you can use existential quantification:

{-# LANGUAGE ExistentialQuantification #-}

data Showable = forall a. Show a => Showable a

instance Show Showable where
  show (Showable x) = show x

test :: main ()
test = let list = [Showable True, Showable "string"]
       in print list

-- >>> test
-- [True,"string"]

Here you can say that dispatch happens "dynamically". It happens in the same way as in C++ or Java. The Show dictionary is carried with an object, like a vtable in C++ (or class definition ptr in Java, dunno how it's called).

Yet, as @MathematicalOrchid explained, this is an anti-pattern.


Yet if you want to flip from Left to Right and back, you can state that in type class definition.

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FunctionalDependencies #-}

class Flippable a b | a -> b where
  flip' :: Flippable b a => a -> b

newtype INL = INL Int deriving Show
newtype INR = INR Int deriving Show

instance Flippable INL INR where
  flip' (INL x) = INR x

instance Flippable INR INL where
  flip' (INR x) = INL x

-- >>> flip' $ INL 1
-- INR 1
-- >>> flip' $ flip' $ INL 1
-- INL 1

In this case flip' calls are resolved already at compile-time.


You could have have class Flip a where flip' :: a -> Flippable using existential quantification too. Then consecutive calls will be dispatched dynamically. As always, it depends on your needs.

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE StandaloneDeriving #-}

class Flip a where
  flip' :: a -> Flippable

-- Show is only for repl purposes
data Flippable = forall a. (Flip a, Show a) => Flippable a

deriving instance Show Flippable

instance Flip Flippable where
  flip' (Flippable x) = flip' x

newtype INL = INL Int deriving Show
newtype INR = INR Int deriving Show

instance Flip INL where
  flip' (INL x) = Flippable (INR x)

instance Flip INR where
  flip' (INR x) = Flippable (INL x)

-- >>> flip' $ flip' $ flip' $ INL 1
-- Flippable (INR 1)

Hopefully this answers your question.

phadej
  • 11,947
  • 41
  • 78
  • Thanks that's awesome - could you clarify re Flipping from Left to Right - could the state of Flippable be determined at runtime? – hawkeye Jan 27 '15 at 12:11
  • You can add a method to `Flippable` interface to determine whether it's left or right. But this sounds already that, you are searching for ADT: `Either a a` or `(Bool, a)` (boolean tells the state). – phadej Jan 27 '15 at 12:18