I don't think Haskellers bash OO that much. They bash some of the trappings of it. GoF is usually a typesafe library in Haskell because it has better abstraction capabilities. Functions form better fundamental units than objects do (but are a form of "object" themselves). Composition is better than inheritance. Classes are not generally so useful.
Really, I think Haskell has a super great object system. It's just (a) not the central conceit and (b) so naturally embedded in the language that you can program for a long time without even noticing it's there.
Codata typically indicates you're looking at an object. In Haskell, due to it being non-terminating as a language, codata and data are unified so most "objects" look identical to their non-object form. You can also notice them by definitions based on eliminators. Automata are a good example
newtype Auto f i o = Auto { run :: i -> f (Auto f i o, o) }
You can also look for objects as ADTs. When you do this you can get a natural embedding of a prototype-based language. (
http://www.cs.ox.ac.uk/jeremy.gibbons/publications/adt.pdf)
data ADT f = forall st
. ADT { unfold :: st -> f st
, state :: st
}
Here, the internal state type `st` is closed over as an existential value—when you create a new ADT you can pick what st is but the type system then ensures that
nobody ever can access that type again. Instead, you have to use the `unfold` elimination form which projects the state into a "class" `f` (represented as a Functor, but that's immaterial) which defines a signature of methods over the abstract state. Auto, from before, is definable this way.
data AutoClass st o =
AutoClass { next :: st
, out :: o
}
type Auto = ADT AutoClass
Subtyping occurs naturally with quantified types like the existentially-typed `st` variable or the universally quantified types you see all the time in type signatures. It's a more natural form of subtyping since it's all defined by increasing typeclass bounds. Something like
{C} : set of constraints Ci : constraint
------------------------------------------------------
forall a . {C} a => a :> forall a . ({C} a, Ci a) => a
This guarantees that these these two types are good "subtypes on eliminators" which satisfies the Liskov Substitution Principle if not some of the more strict definitions of subtype "niceness".
Double/Multiple dispatch is trivially handled in Haskell since polymorphism is solved by an entire Prolog embedded in the type system.
The thing is that Haskell also has initial data, things like finite lists are better thought of as a big tree of constructors
1:(2:(3:(4:(5:[]))))
and then pattern matched upon (defining catamorphisms)
sum :: [Int] -> Int
sum (a:bs) = a + sum bs
sum [] = 0
and this pattern is perhaps a little bit emphasized in mainstream Haskell because it sides on the "functional" side of the expression problem. It doesn't mean, though, that Haskell doesn't like the "object oriented" side, but instead that Haskell favors both.