Daryoush Mehrtash wrote:
> I have had an unresolved issue on my stack of Haskell vs Java that I wonder
> if your observation explains.
>> If you notice java generics has all sort of gotchas (e.g.
>http://www.ibm.com/developerworks/java/library/j-jtp01255.html). I somehow
> don't see this discussion in Haskell. I wonder if haskell's model of
> letting the caller determine the result type has advantage in that you don't
> have all the complexity you would have if you let the API determine their
> types.
That "gotcha" about generics, is really a gotcha about arrays. If you
replace List<> with [] in their example it'll compile just fine, though
it can never work. The runtime happens to catch this particular error as
an ArrayStoreException, but there are more convoluted examples that let
you break the type system in all sorts of disturbing ways. As I said
before, co-/contravariance is a Hard Concept(tm) where intuitions lie.
I think the big difference is that Haskell (similar to some non-Java OO
languages) separates the notion of a value from the notion of associated
functions. In Haskell we expect that the result of a function, say, is a
value with an actual type-- the actual type needed by the caller in
fact, perhaps constructed by a type-class dictionary passed down by the
caller. Whereas in Java the result of a function is some stateful bundle
of functions-- and whatever the type is, the callee returns the
functions that can be used on it. This is highlighted in Haskell by type
signatures like |Foo a => a|, sure the type of our result may have a
bundle of associated functions, but we know that the result is really a
value of type |a|, polymorphically if need be.
For simple object-like types that may not seem so different, but for
container types the difference becomes readily apparent. In Java an
interface needs to cement the collection and so you often see interfaces
with different versions of the same function only offering/accepting
different collections. There are some abstract classes or interfaces
that try to expand on this to make things more polymorphic, but this
perspective still can't escape certain problems...
For example, the fragile base class problem. One of my gripes about Java
is that for some unfathomable reason, Iterators are not Iterable. More
particularly, for iterators from library collections, I often cannot
make them so[1]. In Haskell, since associated functions like iterator()
are not part of the value but instead exist ethereally, we'd be free to
give an instance for all Iterators making them Iterable.
This is the sort of problem that Pythonistas and Rubyists work around
with monkey patching. Another example is, say we have an object that
constructs a priority queue internally, and ultimately hands it off to
the caller; this class doesn't actually care about the pqueue itself,
though it doesn't treat it as a black box either. If someone comes up
with a more efficient implementation of pqueues they can't just require
it (via polymorphism) from that class, instead they need to go in and
change the class that doesn't care[2].
In a sense, Haskell embraces the dread spectre of multiple inheritance.
There are inheritance trees of type-class instances, but the association
of these functions to datatypes is flat, and datatypes can always opt
into additional type-classes. The diamond problem of C++ multiple
inheritance goes away since values never inherit additional fields,
however we do still have diamond problems of our own which
OverlappingInstances and IncoherentInstances try to work around.
Type-classes do specify types in the API, they can even specify types in
ways that Java interfaces cannot (e.g. |a -> a -> b| [3]). But the way
they specify types, functional polymorphism vs inheritance, are
radically different.
[1] Cf. Vector which uses an anonymous local class. In order to have
Vectors that do return iterable iterators, we need to subclass Vector
and override iterator() with an almost identical declaration that adds a
method: public Iterator<E> iterator() { return this; }
[2] Potentially by subclassing, assuming the class doesn't have too many
interdependencies. Or by using some defunctionalization pattern to mimic
caller-directed polymorphism, especially if we want to reuse the same
factory for multiple callers for whom different pqueues are more efficient.
[3] Java has only one top type: Object, and so it can't ensure this
constraint. In some cases generics can be bent to fill this gap, but in
others it's much harder, e.g. |class Foo a where foo :: Int -> a|.
OO-Languages like SmallTalk can better capture some of these because
they treat classes as objects too, but there are still differences.
--
Live well,
~wren