The Expression Problem - Haskell
I've been having fun with Haskell again lately, so when I came across this post (via @rickasaurus) about the expression problem, it got me thinking about how I'd approach the code in Haskell. The functional language example that Robert gives doesn't make use of Type Classes (I believe because SML lacks them, but someone correct me if I'm wrong here), which allow us to achieve nearly the same result as the Magpie example he gives at the end.
According to Robert's post, our goal is to organize the code for 3 operations across 3 data types, then add operations and data types into the mix while ensuring every data type supports every operation. For laziness's sake, I'm going to limit my examples to 2 operations and 2 data types, then add a third operation at the end.
Question 1: How do we organize the code?
Here's some pretty vanilla Haskell code that implements load and save
for two different document types.
> module Main where
> import Prelude hiding (print)
> class Document a where
> load :: a -> IO ()
> save :: a -> IO ()
> data TextDocument = TextDocument String
> instance Document TextDocument where
> load (TextDocument a) = putStrLn ("Loading TextDocument(" ++ a ++ ")")
> save (TextDocument a) = putStrLn ("Saving TextDocument(" ++ a ++ ")")
> data DrawingDocument = DrawingDocument String
> instance Document DrawingDocument where
> load (DrawingDocument a) = putStrLn ("Loading DrawingDocument(" ++ a ++ ")")
> save (DrawingDocument a) = putStrLn ("Saving DrawingDocument(" ++ a ++ ")")
Instead of starting with our data types as in Robert's SML example, we start with the operations we want them to
support. These are collected in the Type Class Document. For the purposes of our goal here, the
Document Type Class is analogous to the Document interface in Robert's post.
We break our documents out into different types rather than just using a single data type like Robert's example.
Doing so allows us to specify different Document instances (implementations) for
TextDocument and DrawingDocument.
Let's address Robert's remaining questions.
Question 2: How do we add new types of documents?
The Open World Assumption (search for "open world" on the page)
allows us to declare new instances of the Document class anywhere in our code, so we just create a new data type and provide
an implementation. Easy.
Question 3: How do we add new operations you can perform on any document?
Well, if we restrict ourselves to saying it has to be part of the Document Type Class, then we're stuck in the same
situation as Robert's OOP example. But that's probably not how would we approach the problem in Haskell. In Haskell we'd be more
likely to declare a new Type Class for the new operation, like so:
> class Printable a where
> print :: a -> IO ()
Then we can take advantage of the Open World Assumption again and declare instances of Printable for all our documents, either
in the same file as Printable, or their own files, or wherever we like.
> instance Printable TextDocument where
> print (TextDocument a) = putStrLn ("Printing TextDocument(" ++ a ++ ")")
> instance Printable DrawingDocument where
> print (DrawingDocument a) = putStrLn ("Printing DrawingDocument(" ++ a ++ ")")
Technically I've cheated because I haven't ensured that every Document supports print. It just so happens
that every type we know about that is a Document is also Printable. That brings us to Robert's
final question.
Question 4: How do we ensure all of the data types support all the operations?
Thanks to Haskell's type inference, as soon as we make use of the
print operation, the type checker figures out that the data type must be Printable and will complain if
it is not. So in fact we will only need to provide Printable instances for data types that we actually try to print.
I actually this is an improvement over adding print to the Document class, but it does come at the cost of
adding an additional interface to the codebase.
Conclusion
We were nearly able to achieve Robert's Magpie solution in Haskell by using Type Classes. We failed only in that
we couldn't add a new operation to Document and had to declare the Printable Type Class to
add print support. I think we did pretty well.
And here's a final bit of code to make an actual program that will put these documents through their paces.
> test a = do
> load a
> save a
> print a
> main = do
> putStrLn ""
> test (TextDocument "text")
> putStrLn ""
> test (DrawingDocument "drawing")
> putStrLn ""
Happy Haskelling!