Functional Anti-Patterns
which I will be talking about shortly, but first I need to admit that for the past several years I have been quite wrong. It has been my (oft-expressed to anyone who would give even the semblance of listening) opinion that programming languages like Haskell are a Very Good Thing; that by making it natural to express algorithms in a way closer to the mathematics underlying their formal models, it makes it easier to create programs that could be reasoned about relatively easily and with relatively little programmer error; that thus equipped, programmers could produce better code in less time. In short, I have claimed, Haskell is an enabling tool for good programming.
The truth of the matter being, however, that I was titanically wrong. While it is possible for a good Haskell programmer to accomplish more in less time with a greater likelihood of success in Haskell than in, oh, say, for example, Java, it is not correspondingly true that a bad programmer will produce better code in less time than they would in oh, say, for example, Java. Rather, Haskell requires that bad programmers spend time that they could have spent writing bad Java code inventing exciting new ways to write bad code, specially tuned to the mechanisms of functional programming. My point is to elaborate on a few of those while taking the opportunity to expound on why Haskell is really a good language anyway and it's not the poor language's fault that such evil things were done with it.
My first target is a design pattern I like to refer to as (or not). This pattern simply consists of adding an implied (or not) to the end of each data type name. For example, you might be tempted to read a declaration that started
data Shape = ... as "data Shape is," but when dealing with a practitioner of the (or not) pattern, you would be incorrect. This ought to be read "data Shape (or not)," which is also represented in the Shape data structure:
data Shape = ... | NoShape
This provides the immense benefit that you do not ever have to indicate when things can (or have) actually failed! Failure is implicit in everything, and instead of being handled once, it can be handled in every function ever written ever!
boundingBox :: Shape -> Shape
boundingBox (Rect topLeft bottomRight) =
Rect topLeft bottomRight
boundingBox (Circle (x,y) radius) =
Rect (x - radius, y - radius)
(x + radius, y + radius)
...
boundingBox NoShape =
NoShape
(Although a proper practitioner of (or not) would realize quickly that I have not allowed for failure in my type for points, and I should be typing something like Point x y (to allow for NoPoint).) It goes without saying that there is, in fact, a better way to handle this. It's a particularly nice way, though, so I'd like to discuss it a bit. The proper way to do this is to first decide that perhaps it would be nice to know when failure is going to happen, and that second once we've formalized failure in that manner, it might be possible to handle it more convientently.
There are any number of ways to formalize failure. I'm going to use the standard prelude's Maybe type:
data Maybe a = Just a | Nothing
which would seem to be the same thing as (or not) except that whereas (or not) is implicit, Maybe is explicit and thus it is possible to have functions like boundingBox which are, in fact, complete functions and do not have to worry about the failing case. Further, functions which can fail, such as a hypothetical
parseShape :: String -> Maybe Shape
not only indicates that it fails, but forces the user to actually do something about failure instead of helpfully passing it on for some other function some day to discover and perhaps maybe do something about except probably it will just pass it on and the only way you'll discover that you didn't parse a shape is when you get to the other end and find that in fact the best strategy for getting from here to there is NoStrategy.
Of course, introducing Maybe has its own downside, which often looks like this:
case parseShape str1 of
Nothing -> Nothing
Just s1 ->
case parseShape str2 of
Nothing -> Nothing
Just s2 -> boundingBox s1 s2
but even this is, in fact, abstractable, and can be reduced to
do s1 <- parseShape str1
s2 <- parseShape str2
return (boundingBox s1 s2)
via monads. I would tell you about monads but everyone else has already written a blog post about that and I don't feel like it right now. In this particular case, it's particularly easy because all the necessary plumbing is built right into the standard library.
Of course, a form of this generalization is, in fact, possible for the (or not) pattern. It's not quite as nice as the Maybe approach, what with not using the type system and not actually guaranteeing that failure is handled, but as long as we're relying on the programmer to include failure cases, we might as well make it easy.
First, we abstract the notion of failure. We'll do that with a type class:
class Fuckable f
where fuck :: f
isFucked :: f -> Bool
Then, we can provide functions to automagically handle failure for functions that wouldn't otherwise:
liftF1 :: (Fuckable f, Fuckable g)
=> (f -> g) -> f -> g
liftF1 f x
| isFucked x = fuck
| otherwise = f x
liftF2 :: (Fuckable f, Fuckable g, Fuckable g)
=> (f -> g ->h) -> f -> g -> h
liftF2 f x y
| isFucked x || isFucked y = fuck
| otherwise = f x y
...
and now, we can omit the last case from the earlier boundingBox function, instead just writing
boundingBox = liftF2 boundingBox'
where boundingBox' = ...
and I'm sure we can all agree that is better than anything in any "standard prelude" you or I or anyone else might be interested in snarkily bringing up as containing "better" ways to do things.

6 diversions:
Well, you're not alone in having made this mistake -- I think anybody who cares a lot about programming languages has developed the delusion that if only we could design a programming language just so, we could prevent programmers from making the kinds of mistakes we've had to painfully work around in our professional lives. I've certainly done it, and I know plenty of others. Welcome.
Python devotees argue that their syntax prevents programmers from writing the kinds of tangled wads of illegible punctuation that pass for programs in C and Perl. Lovers of Haskell believe that by enforcing pure functional programming, they can lead the flock away from the imperative excesses of Scheme and Common Lisp. Even the improbable ugliness of C++ was created out of a misguided desire to lead the unwashed programmer to a baptismal betterment that never came. Instead, they figured out how to encode Peano arithmetic in the template system. Dear God, shoot me now.
Unfortunately, Fred Brooks was right: these silver bullets never work. The harder you work to make your language foolproof, the more ingenious the fools become. Your functional anti-patterns are yet another superb demonstration of this inescapable fact: The only way to get better programs is to train better programmers. A good enough programmer can squeeze beauty out of even the most unlikely tangle of broken syntax and semantics. But this requires discipline, even in a language that does not set you loose like a hyperactive child in a roomful of unguarded power tools the way C does.
Does that mean we should give up and go home? Hell no. But we need not beat ourselves up if some brainless schmuck figures out a clever way to utterly bypass our beautiful type system so he can compile the BASIC programs of his youth in GHC. Let them paint their dunce caps bright red, if that's their wish. We can hardly prevent it, so we may as well relax to it.
I think I'll share this article with my coworkers...whoa! Maybe not!
Who knew that Haskell was so NSFW?
Cheers.
I'm not quite sure I understood what you're trying to say, but there *is* a liftM, you know.
liftM f m1 = do { x1 <- m1; return (f x1) }
That desugars into
liftM f m1 = m1 >>= (return . f)
Now, as you said yourself, the Maybe type is an instance of Monad. The definitions -- which can be found in the incredibly readable GHC source code -- for the Monad operations in the Maybe type are:
instance Monad Maybe where
(Just x) >>= k = k x
Nothing >>= _ = Nothing
(Just _) >> k = k
Nothing >> _ = Nothing
return = Just
fail _ = Nothing
Evidently, that does what you want.
But wait, it gets even better! Maybe (as all monads) is also an instance of Functor -- which basically means it has a fmap. Look what the fmap operation on Maybe looks like:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
D'oh.
It gets even better :)
fmap is equivalent to liftM, for types satisfying both Monad and Functor (and if your monads aren't functors, shame on you).
Diego: Beware the sarcasm. :-)
Diego: Yes, the code in question would have better written using the Maybe monad - probably even better using the Either monad. My point was more to poke fun at the squareness of the wheels (and the "patterns" people in general) than to suggest that we ought to make our roads more supportive of them.
jdf: I considered a "Fails" or "Fouled" class instead, but I thought it lost something in the process...
Post a Comment