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
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
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 is also
Printable. That brings us to Robert's
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
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
Document class, but it does come at the cost of
adding an additional interface to the codebase.
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
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 ""