For example, why isn't it possible to define a function like -
fun::a
fun = 1
-- OR
someInt::Int
someInt = 3
fun::Num a => a
fun = someInt
It seems like this is possible in Java -
class App {
public static void main(String[] args) {
System.out.println(new SomeClass().hello().sayHi());
}
}
class SomeClass {
Hi hello() {
A a = new A();
return a;
}
}
interface Hi {
String sayHi();
}
class A implements Hi {
public String sayHi() {
return "Hi from A";
}
}
class B implements Hi {
public String sayHi() {
return "Hi from B";
}
}
but the equivalent in Haskell does not work -
main = print $ sayHi hello
hello:: Hi a => a
hello = A
class Hi a where
sayHi::a -> String
data A = A
data B = B
instance Hi A where
sayHi _ = "Hi from A"
instance Hi B where
sayHi _ = "Hi from B"
I'm trying to understand the fundamental difference between interfaces in Java and typeclasses in Haskell? What are the limitations and benefits of each approach?
This is a very common misunderstanding.
Polymorphism works completely different in Haskell compared to OO languages. (Though many of these languages have a templating / generics feature that is more comparable to Haskell's polymorphism.)
In Java, polymorphism is based on subtyping: by defining an interface I
, you define a (large, abstract) type, and a class that instantiates this interface defines a subtype of I
. So, a Java interface isn't fundamentally different from a Java class – indeed, in C++ interfaces and classes use the same class
keyword; Java merely places some sensible restrictions on how each can be used.
That means polymorphic functions don't really need to do anything else from ordinary functions: they just have a signature that refers to the type I
, which includes any subtype. So if I
is supposed to be the result type, the implementor has the freedom to return any subtype.
In Haskell, there is no such thing as subtypes† (though there are ways to emulate them). Typeclasses don't define types at all (but rather sets of types instead; the distinction is important for much the same reason it's important to make a distinction between e.g. integers and lists of integers). So they can't appear in signatures the same way as types would. Haskell polymorphic functions really do work entirely different from monomorphic ones, and the signature actually expresses this quite clearly:
fun :: Num a => a
says that fun
first takes an (implicit, type-level) argument Num a
, and then produces a value of type a
.
...Actually even a bit more is happening: the full form of the above signature is
{-# LANGUAGE ExplicitForall, UnicodeSyntax #-}
fun :: ∀ a . Num a => a
which says basically it takes two arguments:
a
Num a
, which explains how it is possible to use a
as a number typea
. That is, the exact type a
, not some subtype or whatever.When calling fun
, you don't need to explicitly pass either of these arguments, the compiler can do that for you, but it does need to be clear what particular type it should be. You can explicitly pass the type argument, that's what the type applications syntax is there for:
{-# LANGUAGE TypeApplications #-}
fun :: ∀ a . Num a => a
fun = 37 + 9
main :: IO ()
main = do
print (fun @Int) -- prints 48
print (fun @Float) -- prints 48.0
... not to be confused with (fun :: Int)
, though that has in this example exactly the same effect: here, the compiler infers from the context that a
must be the type Int
because that's expected as the result, whereas fun @Int
explicitly instructs the compiler to use Int
and not allow it to adapt to the environment.
This way of doing polymorphism gives the caller a lot of power, but of course the flip side is that the callee needs to support all that generality by supporting all the types that the caller could possibly request.
It's not usually a big problem, just don't try to squeeze Haskell code into the mindset you would have in a language like Java. A class or interface in Java often corresponds best to a simple data
structure, or indeed just a value of an already existing type, not any typeclass.
For your example, why not just use:
data Hi = A | B
sayHi :: Hi -> String
sayHi A = "Hi from A"
sayHi B = "Hi from B"
What I've done here is basically just declared A
and B
as something analogous to subtypes of the type Hi
, right there and then – not by first defining Hi
abstractly and then post-hoc populating it with subtypes, but by saying up front that Hi
consists of both A
and B
.
Alternatively, if you want to keep Hi
open, i.e. want to allow people downstream to customize it in unforseen ways... well, then it's also completely unforseen what kinds of strings you get, so what's the point of having a type-level abstraction at all? You might just as well take the strings as arguments as they are, which makes it even simpler:
sayHi :: String -> String
sayHi x = "Hi from "++x
†To be precise here: in Haskell, Rank-0 types do not have subtypes. A rank-0 type is what could also be described as a concrete type, whereas higher-rank types have quantors in them. Higher-rank types do have subtypes (basically, by quantifying over different classes). But it's kind of begging the question, because a rank >0 type has polymorphism already built in, as it were.