(created new article; dunno if this is the right place; please improve)
Revision as of 23:37, 10 November 2008
One cool thing you can do with type classes and instances in Haskell is to make functions that seemingly take any number of arguments, like the "printf" function in Haskell.
For example, here is a function which takes any number of Show-able arguments, and then prints them, one per line, to stdout:
class PrintAllType t where printAll' :: [String] -> t instance PrintAllType (IO a) where printAll' acc = do mapM_ putStrLn acc return undefined instance (Show a, PrintAllType r) => PrintAllType (a -> r) where printAll' acc = \x -> printAll' (acc ++ [show x]) printAll :: (PrintAllType t) => t printAll = printAll'  main :: IO () main = do printAll 5 "Mary" "had" "a" "little" "lamb" 4.2 -- note: the arguments can even be different types printAll 4 3 5
Here "PrintAllType" is a type class we created specially for the use of this variable-argument function. Types of this type class represent functions that can take zero or more arguments.
The type class specifies a function, here called "printAll'", which takes as an argument some kind of accumulated state of the arguments so far, and returns the type of the type class. Here I chose to accumulate a list of the string representations of each of the arguments; this is not the only way to do it; for example, you could choose to print them directly and just accumulate the IO monad.
We need two kinds of instances of this type class:
- There are the "base case" instances, which has the type that can be thought of as the "return type" of the vararg function. It describes what to do when we are "done" with our arguments. Here we just take the accumulated list of strings and print them, one per line. (We actually wanted to use "IO ()" instead of "IO a"; but since you can't instance just a specialization like "IO ()", we used "IO a" but return "undefined" to make sure nobody uses it.)
- You can have multiple base case instances; for example, you might want an instances that returns the result as a string instead of printing it. This is how "printf" in Haskell can either print to stdout or print to string (like sprintf in other languages), depending on the type of its context. So you can kind of have a function that returns multiple types.
- The other kind of instance is the "recursive case". The type is a function type of the argument type to another type of our type class. It describes what happens when you come across an argument. Here we simply append its string representation to the end of our previous "accumulated state", and then pass that state onto the next iteration. Make sure to specify the requirements of the types of the arguments; here I just required that each argument be an instance of Show (so you can use "show" to get the string representation), but it might be different for you. Again, you can have multiple "recursive case" instances, for different types. One interesting thing to note is that since the type requirement is processed for each argument individually, we can have arguments of different types.
Note the resemblance between our function above and the following function that operates on a list (except this one requires its args to be the same type):
printAllList :: (Show a) => [a] -> IO b printAllList = printAllList'  where printAllList' acc  = do mapM_ putStrLn acc return undefined printAllList' acc (x:xs) = printAllList' (acc ++ [show x]) xs