3

Let's say I need different output depending on the type of the polymorphic parameter of a function. My initial attempt fails with some cryptic error message:

choice :: a -> Int
choice (_ :: Int) = 0
choice (_ :: String) = 1
choice _ = 2

However, we can fix that easily by wrapping the desired types in different data constructors and use those in pattern-matching:

data Choice a = IntChoice Int | StringChoice String | OtherChoice a

choice :: Choice a -> Int
choice (IntChoice _) = 0
choice (StringChoice _) = 1
choice (OtherChoice _) = 2

Question: Do you know of a way to circumvent this? Is there a feature in Haskell2010, GHC or any of the extensions that allows me to use the first variant (or something similar)?

mucaho
  • 2,119
  • 20
  • 35
  • 1
    `Data.Typeable` allows to query types at run-time, but it shouldn't be used lightly. – chi May 19 '15 at 18:44

2 Answers2

6

This is confusing two different kinds of polymorphism. What you want is ad-hoc polymorphism, which is done through type classes. The kind of polymorphism of a function of type a -> Int is parametric polymorphism. With parametric polymorphism, one function definition for choice must work for any possible type a. In this case, this means it can't actually use the value of type a since it doesn't know anything about it, so choice would have to be a constant function such as choice _ = 3. This actually gives you very strong guarantees about what a function can do, just by looking it its type (this property is called parametricity).

With a type class, you can implement your example as:

class ChoiceClass a where
  choice :: a -> Int

instance ChoiceClass Int where
  choice _ = 0

instance ChoiceClass String where
  choice _ = 1

instance ChoiceClass a where
  choice _ = 2

Now, I should point out that this type class approach is often the wrong one especially when someone who is just starting wants to use it. You definitely don't want to do it to avoid a simple type like the Choice type in your question. It can add lots of complexity and instance resolution can be confusing at first. Note that in order to get the type class solution to work, two extensions needed to be turned on: FlexibleInstances and TypeSynonymInstances since String is a synonym for [Char]. OverlappingInstances is also needed because type classes work on an "open world" assumption (meaning that anyone can later come along and add a instance for a new type, and this must be taken into account). This is not necessarily a bad thing, but here it is a sign of the creeping complexity caused by using the type class solution over the much simpler data type solution. OverlappingInstances in particular can make things harder to think about and work with.

David Young
  • 10,713
  • 2
  • 33
  • 47
  • 1
    You also need `OverlappingInstances` to actually use that class -- I find this extension particularly tricky and hard to fully grasp. E.g. http://stackoverflow.com/questions/29504107/which-dictionary-does-ghc-choose-when-more-than-one-is-in-scope – chi May 19 '15 at 18:38
  • @chi Thanks for bringing that up! For a moment I thought it wasn't necessary because `choice 'a'` worked without it (even though I originally thought it would be). It isn't possible to use the other two instances without it though. I find that extension hard to understand too and I definitely think it adds additional complexity that should be avoided if possible. I updated my answer to mention it. – David Young May 19 '15 at 18:47
  • Thanks for the valuable information. So you are suggesting stick to data constructors if possible, right? – mucaho May 19 '15 at 20:23
  • Regarding `OverlappingInstances` - take a look at the [`Safe` GHC extension](https://ghc.haskell.org/trac/ghc/wiki/SafeHaskell#RestrictedandDisabledGHCHaskellFeatures) - it restricts those (all?) cases where more specialized typeclass instances in a referring module could lead to run-time errors – mucaho May 19 '15 at 20:51
  • 1
    @mucaho That has to do with interactions between modules and which instances can be exported by a module. That is interesting, I didn't think there was any case where you could prevent an instance from being exported. That's probably the only time that you can prevent it. Also, yeah, I would definitely suggest sticking to data constructors. Type classes are almost exclusively for when you have an *very* general interface that will likely be able to be expanded with new instances by other people. Good examples include `Monoid` and `Functor`. – David Young May 19 '15 at 22:01
4

Question: Do you know of a way to circumvent this? Is there a feature in Haskell2010, GHC or any of the extensions that allows me to use the first variant (or something similar)?

No, there is no feature either in Haskell 2010 or provided by any GHC extension that lets you write a function of type

choice :: a -> Int

whose return value depends on the type of its argument. You can count on such a feature never existing in the future either.

Even with hacks to inspect GHC's internal data representation at runtime, it's impossible to distinguish a value of type Int from a value whose type is a newtype of Int: those types have identical runtime representations.

The Int returned by your function is a value that exists at runtime, so it needs to be determined by another value that exists at runtime. That could be either

  1. an ordinary value, like in your data Choice a = IntChoice Int | StringChoice String | OtherChoice a; choice :: Choice a -> Int example, or

  2. a type class dictionary, using either a custom class as in David Young's answer, or the built-in Typeable class:

    choice :: Typeable a => a -> Int
    choice a
      | typeOf a == typeOf (undefined :: Int)    = 0
      | typeOf a == typeOf (undefined :: String) = 1
      | otherwise                                = 2
    
Reid Barton
  • 14,951
  • 3
  • 39
  • 49
  • Thank you for clarifying the alternatives. _"it's impossible to distinguish a value of type Int from a value whose type is a newtype of Int: those types have identical runtime representations"_ - isn't that what `TypeSynonymInstances` is for? (mentioned in [david's answer](http://stackoverflow.com/a/30333292/3041008)). Would you be so kind and provide a definition for the `choice` function using the `Typeable` type class? – mucaho May 19 '15 at 20:32
  • 3
    @mucaho `TypeSynonymInstances` is specifically for type class instances. In particular, it lets you make an instance for a type synonym. A type synonym is a type that is treated the same as another type by the type system (in this case, `type String = [Char]`). Even with that extension and `OverlappingInstances`, you can't make an instance of both `String` and `[Char]` though, since they are synonymous. `newtype`s, on the other hand, *are* treated as different types by the type system but they have the exact same runtime representation. – David Young May 19 '15 at 21:53