> What I've done is used a record type to encode the interface that it's supposed to expose,
I love how Haskellers (in general, not you in particular) bash OO and then come up with the exact same technique of simulating OO that is used in C (a struct of function pointers). Something which OO languages provide out of the box (interfaces, virtual methods, etc).
Now, let's take this a step further: how would you simulate double-dispatch, or multiple dispatch in Haskell? This is sorely missing from mainstream OO languages.
I can't speak for the community as a whole, but the Haskellers I know generally "bash" OO with respect to its use of pervasive mutable state and open recursion, and not because the notion of "object" is an inherently wrong one. Those features must be explicitly included in the above model of object (by manually including reference cells and manually invoking a "method" with its own "instance", respectively) whereas they are implicitly included in every object in most commonly-used OO languages.
As for multiple dispatch, you could always use multi-param type classes:
{-# LANGUAGE MultiParamTypeClasses #-}
class Say a b where say :: a -> b -> IO ()
instance Say Int () where say = putStrLn "Case one"
instance Say Int Int where say = putStrLn "Case two"
which corresponds roughly to the CLOS snippet
(defgeneric say (a b))
(defmethod say ((a integer) (b null)) (format t "Case one"))
(defmethod say ((a integer) (b integer)) (format t "Case two"))
I do not believe that there is a way to use the record-based model of objects I showed earlier to do the same thing, but perhaps there is some way that is presently eluding me.
There is a slight technical difference that winds up being a more significant practical difference between the C solution (struct of function pointers) and the Haskell solution (record of Haskell functions): the Haskell functions can close over arbitrary data.
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) }
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.
I love how Haskellers (in general, not you in particular) bash OO and then come up with the exact same technique of simulating OO that is used in C (a struct of function pointers). Something which OO languages provide out of the box (interfaces, virtual methods, etc).
Now, let's take this a step further: how would you simulate double-dispatch, or multiple dispatch in Haskell? This is sorely missing from mainstream OO languages.