The normal way to "pattern match" on types in the way you're describing is with type class instances. With concrete types, this is easy using MultiParamTypeClasses
; this is how Haskell implements multiple dispatch.
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, OverlappingInstances #-}
module SO26303353 where
class (Num a, Num b) => Power a b where
my_pow :: a -> b -> a
instance Power Double Double where
my_pow = (**)
instance Num a => Power a Integer where
my_pow = (^)
This works just fine. It's more or less idiomatic Haskell, except that (**)
and (^)
are different operations and some people might object to blurring the distinction.
You're asking for something a bit more elaborate, however. You want multiple dispatch not only on types but on classes of types. This is a significantly different and more powerful thing. In particular, it would work for all types that could have instances of Floating
or Intergral
, even types that haven't been written yet! Here's how it would be written ideally:
instance (Floating a) => Power a a where
my_pow = (**)
instance (Num a, Integral b) => Power a b where
my_pow = (^)
This doesn't work, though, because the constraint solver does not backtrack, and does not consider instance constraints when choosing an instance. So my_pow
doesn't work, for instance, with two Int
s:
ghci> :t my_pow :: Int -> Int -> Int
No instance for (Floating Int)
This happens because the "more specific" Power a a
instance matches, because the two types are equal. GHC then imposes the Floating
constraint on a
, and barfs when it can't satisfy it. It does not then backtrack and try the Power a b
instance.
It may or may not be possible to hack around the limitation using advanced type system features, but I don't think you could ever make a drop-in replacement for both (**)
and (^)
in current Haskell.
Edit: general comments
(Note that we're kind of straying away from a Q&A format here.)
In rereading your question and comment, I notice you're using the term "dispatch" in a way I'm not familiar with. A quick Google turns up articles on double dispatch and the visitor design pattern. Is that where you're coming from? They look a bit like what you're trying to do--write a function that does totally different things based on the types of its arguments. I want to add a few things to this answer that may help hone your sense of idiomatic Haskell. (Or may just be disjointed rambling.)
Haskell normally disregards the idea of a "runtime type". Even in @Cirdec's more elaborate answer, all the types are statically known, "at compile time." (Using the REPL, ghci
, doesn't change things, except that "compile time" gets kind of hazy.) In fact, intuitions about what happen "at runtime" are often different in Haskell than other languages, not least because GHC performs aggressive optimizations.
Idiomatic Haskell is built on a foundation of parametric polymorphism; a function like replicate :: Int -> a -> [a]
works absolutely the same for any type a
. As a result, we know a lot about what replicate
does without having to look at its implementation. This attitude is really helpful, and it deeply infects the brains of Haskell programmers. You'll notice that me and many other Haskell programmers go crazy with type annotations, especially in a forum like this one. The static types are very meaningful. (Keyword: free theorems.) (This isn't immediately relevant to your question.)
Haskell uses type classes to permit ad hoc polymorphism. In my mind, 'ad hoc' refers to the fact that the implementation of a function may be different for different types. This is of course critical for numerical types, and has been applied over the years in countless ways. But it's important to understand that everything is still statically typed, even with type classes. To actually evaluate any type-class function--to get a value out of it--you need to in the end choose a specific type. (With numeric types, the defaulting rules frequently choose it for you.) You can of course combine things to produce another polymorphic function (or value).
Historically, type classes were thought of strictly as a mechanism for function overloading, in the sense of having the same name for several distinct functions. In other words, rather than addInt :: Int -> Int -> Int
, addFloat :: Float -> Float -> Float
, we have one name: (+) :: Num a => a -> a -> a
. But it's still fundamentally the same idea: there are a bunch of completely different functions called (+)
. (Now we tend to talk about type classes in terms of "laws," but that's a different topic.) There's oftentimes no literal dispatch occurring with a function like (+)
, or even non-primitive functions.
Yes, type classes are a bit like interfaces, but don't allow an OOP mindset to creep in too far. If you are writing a function with a type like Num a => a -> a
, the expectation is that the only thing you know about a
is that it is an instance of Num
. You can't look behind the curtain, as it were. (Without cheating. Which is hard.) The only way to manipulate values of type a
is with fully polymorphic functions and with other Num
functions. In particular, you can't determine whether a
is also an instance of some other class.
The various compiler extensions we've been playing with blur this model a bit, because we now can write, essentially, type level functions. But don't confuse that with dynamic dispatch.
Oh, by the way, Haskell does support dynamic types. See Data.Dymamic
. To be honest, I've never really seen much use for it outside of interop with other languages. (I'm willing to be wrong.) The typical "visitor pattern" problems can be implemented in other ways.
Floating
andIntegral
? How would the compiler know which to use? This is particularly the case if I implemented(^)
and(**)
to have different behavior. While this makes no sense from a mathematical point of view, in general this can be something that happens within an application with typeclasses. – bheklilrNum
does not make you aFloating
. Being aFloating
makes you aNum
. There is a hierarchy, it just goes from most general to most specific, and only with type classes. There is no direct type inheritance in Haskell, so you can't say that anInt
is also aFloat
, even though there is a lossless conversion fromInt
toFloat
. Instead, you can convert anInt
to aNum a => a
, and sinceFloat
is an instance of that it can be substituted in as being more specific. – bheklilr