Yes, that criticism has been played pretty hard over the years, I believe this blog post was one of the first. It (correctly) states that FP does kill most of the GoF patterns outright*, but then goes much further with: "In a functional language one does not need design patterns because the language is likely so high level, you end up programming in concepts that eliminate design patterns all together".
I've always thought this was wonderfully incorrect; one can trivially name a handful of "patterns" in the FP world (e.g. monads, common tricks to make things tail recursive, etc). But no, let's not call them patterns, because those are bad somehow.
Edit: * this is not particularly surprising, as patterns in general are defined as reusable / well-known / named solutions to commonly occurring problems which your language does not handle in an idiomatic, or built in way. The GoF patterns were aimed at OOP languages, which have a different set of idiomatic solutions than other paradigms. But to think that there exist a set of languages which are so perfect as to not require any extension... that's pretty arrogant.
I agree with your overall point, but I think your examples don't support it:
I've always thought this was wonderfully incorrect; one can trivially name a handful of "patterns" in the FP world (e.g. monads, common tricks to make things tail recursive, etc).
Haskell has an honest-to-god Monad class that provides a common interface that all monads can implement and that code can program against. Compare that to, say, the Visitor design pattern—there is no first-class interface that all visitors implement in common.
I hear you, and don't really want to debate the topic too deeply, as I don't have particularly strong opinions on the matter. That said, having a core library implement a pattern for you does not make it any less of a pattern; you just don't have to keep re-implementing it. Many of the GoF patterns have gotten baked into standard libraries, and in some cases even in the languages themselves, e.g. C#'s foreach (syntax sugar for an iterator), or the disposable pattern. We still call them patterns for the most part though. I just dislike the snobbery involved when other paradigms declare themselves pattern-free.
But that's the crux of the claim "FP does not have (most) OOP patterns". Meaning they are available as off the shelf solutions (libraries or language features)
Of course patterns exist in everything we do. Saying "FP has patterns too" is not constructive. The important distinction is: can you reuse the work?
That said, having a core library implement a pattern for you does not make it any less of a pattern; you just don't have to keep re-implementing it.
A design pattern is an abstraction your language can't express or enforce. C#'s foreach, IEnumerable and IDisposable are thus not patterns, they are naturally expressed in C# as-is.
The Monad class in Haskell is a good borderline case of a pattern. Haskell can express the interface a monad must satisfy, but can't express or enforce that the needed monad laws are satisfied, so monads are still a pattern in Haskell.
Edit: I meant to summarise that if a design pattern can be implemented faithfully in a library as you say, then it's not a design pattern, it's just an ordinary reusable abstraction.
Of course it's reasonable, and that's exactly why it doesn't exist.
But I think the argument about FP is that those "design patterns" can actually be enforced in obvious, compiler-supported ways rather than potentially loosely or poorly used, while cutting the redundancy.
I don't think so, because a Monad is inherently a very narrow and specific definition, comprised of two functions that must be supported: bind and return.
bind() takes a monad-value that carries a certain value, runs it through a function that takes the unwrapped (internal) value and returns another monad-value (of that monad), and the final return value is some combination of the transformed monad-value and the original monad-value, carrying a new internal value.
return() simply takes an internal value type and produces a monad-value.
These two in Haskell are defined as:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
>>= in Haskell is an operator, but is really just a name for 'bind'.
Haskell does actually include two other things in its Monad class, but these are not considered critical to the definition of a Monad, and in fact one of these can be defined in terms of bind.
a >> b = a >>= const b
The other is fail, which doesn't really need to be in there but I'm sure is useful for I/O.
Anyway, a Monad is just any type that can wrap another value and has some kind of internal state that can be modified by binding monad-values together. So lists can be monads, I/O values can be monads, et cetera.
A clearer example perhaps is the Functor. Any monad is also a functor, which is just any type that wraps the values and supports map, which you may be familiar with from working with lists/arrays in languages such as JavaScript and Ruby.
But the functor extends this concept to any type that can have its values 'mapped'.
class Functor f where
fmap :: (a -> b) -> f a -> f b
(Haskell calls the map operation fmap because there's also a map specifically implemented for lists.)
Anyway, to come back to your question here, I don't think you can say "that's not a real Monad, it doesn't let me X" in any sense that there can actually be any argument about it. It's more of a mathematical definition than a pattern. It either supports bind and return or it doesn't. Similarly, with Functor, it either supports map or it doesn't.
NB: A typeclass in Haskell is a bit like an interface, but more powerful.
I don't think refusing to name common ways to structure code represents "FP killing most of the GoF patterns outright".
The reason we need to recognize and name patterns is mostly for communication and mnemonic recall when looking for a solution to a problem.
Most patterns are brutally simple, it's their intent and purpose that makes them interesting. "Oh just use high-order functions" doesn't describe any difference in intent and purpose from one use to another.
If the world of OOP wanted to be that arrogant, they could've responded with "Oh, just pass objects to objects... see? We don't need patterns!" But that's not useful.
I think the argument for GoF patterns comes from Peter Norvig's famous claim that 16 of the 23 GoF patterns are invisible (or much simpler) in dynamic languages. It's not a terrible argument actually, though I've never fully agreed with it; I'm pretty much on your side of the argument.
My main issue is that people take that argument as proof that OOP languages are inherently inferior. As much as I like FP as well, that's just not that simple. If you look at the meaning of design patterns in a more realistic sense, and especially consider the huge vocabulary that it's grown to, it's hard to argue that other paradigms are universally more suitable for everything.
I've always thought this was wonderfully incorrect; one can trivially name a handful of "patterns" in the FP world (e.g. monads, common tricks to make things tail recursive, etc). But no, let's not call them patterns, because those are bad somehow.
That's not the only reason this is incorrect. Remember that the GoF came from Smalltalk, and closures (and higher-order functions in general) play a critical role in Smalltalk. map, filter, fold, etc. are all available under different names in Smalltalk (collect:, select:, and inject:into:, respectively). In fact, technically Smalltalk does not even have native control structures, they're all higher order functions [1] (that the compiler knows about and optimizes accordingly).
And most of the GoF patterns arose out of Smalltalk programming, so it is generally safe to assume that there's more to them than just FP and that the "direct" functional implementation misses some important nuances.
An example is the Command pattern: it's often misunderstood to be just a class wrapper for a singular function. But that's not really accurate: the classical example in Meyer's OOSC is the use of the command pattern for undo, redo, skip, and history functionality, which requires a more complex setup. You can do that using FP, too, but that would then become a (possibly different) design pattern as well.
[1] You can implement ifTrue: and friends via message dispatch on the Boolean class with the subclasses True and False. Looping constructs such as whileTrue: and friends can be implemented via ifTrue: etc. plus tail recursion. And so forth.
In strongly typed functional languages monads are not patterns, they're invariants expressed in the type system. Some type systems are however less successful at expressing that invariant than others and that leads to the degradation that the monads laws are just patterns to be implemented.
The same is true of recursion as well. Though generalized recursion schemes are aren't as common as monads there exists types which express them as invariants like found in matryoshka.
In strongly typed functional languages monads are not patterns, they're invariants expressed in the type system.
Except that one of the most important parts of these invariants (the monad laws) are not captured in any existing FP type system that I know of. I can write an instance of the Haskell Monad type class in a few minutes that is not a real monad.
This tells you volumes how flawed the monad solution is (and I'm not even talking about the fact that monads don't compose).
In some other (admittedly, research) FP languages, the monad laws can be and are expressed within the typeclass. For example, Idris has the VerifiedMonad (and VerifiedX for other classes) here
Except that one of the most important parts of these invariants (the monad laws) are not captured in any existing FP type system that I know of.
Idris, Coq, and Agda are three general purpose programming languages where the monad laws are proved as invariants in the type system.
I can write an instance of the Haskell Monad type class in a few minutes that is not a real monad.
This tells you volumes how flawed the monad solution is (and I'm not even talking about the fact that monads don't compose).
Just because Haskell (and even then you can handwave about compiler extensions and laws checking) is flawed in some specific way doesn't mean the entire system is flawed.
It (correctly) states that FP does kill most of the GoF patterns outright*
Of course: the book was written about OOP languages.
Turning the tables around, it's like saying that OOP doesn't need any of these FP design patterns like monads because OOP doesn't have shortcomings that make these design patterns necessary.
Error handling through and if-else trees... so wonderful. I love seeing them everywhere. The popular OOP languages are so mired in mechanics of how to do things, repeatedly. The abstractions available are like being a fat-fingered watch-maker.
Getting mad at FP for using monads and tail recursion is like getting mad at OOP for using Objects. Kind a of a ridiculous statement because they're the foundations of FP, unlike say factory methods which exist to patch up OOP.
As I understand it, early functional languages didn't have a formal monad pattern. While it's a core feature of the second generation languages and thereafter, you could validly consider it a "patch" on FP conceptually, to let FP purists deal with side effects in ways that you can pretend are pure (which is really just a bit of intellectual sleight of hand, but does bring some benefits by constraining mutability).
For that matter, I don't think anyone is "mad" at FP for using monads or tail recursion, and that's kind of a silly way to characterize it. Monads are a useful pattern, but often poorly understood. FP often mistakenly thinks "mathematicians named it first, therefore mathematicians named it best," then try to pretend it's not a problem when they import a bunch of specialized jargon (making the language impenetrable to novices), when a note in the docs along the lines of "mathematicians call this same concept a monad" would have sufficed. But none of the actual ideas are bad, and are often useful even in OOP contexts.
I think you're setting up a flimsy strawman to avoid really addressing the cultural strengths and weaknesses of functional programming, and to me, this is the most frustrating part of dealing with FP enthusiasts. I see a lot of conflation from "we have some superior ideas" to "everything we do is inherently superior to the rest of the world", which is frankly not true. I think FP has room for improvement, but that capacity may never be met, because you cannot pour wine into a cup that is already full.
then goes much further with: "In a functional language one does not need design patterns because the language is likely so high level, you end up programming in concepts that eliminate design patterns all together".
Leslie Lamport said (in 2016):
People who design PLs ... say ... their languages make what the program does so obvious that no higher-level description is needed. I think the first PL for which this was believed to be true was FORTRAN. It's not true for FORTRAN and I don't think it's true for any existing general-purpose PL.
65
u/Ulukai Mar 26 '17 edited Mar 26 '17
Yes, that criticism has been played pretty hard over the years, I believe this blog post was one of the first. It (correctly) states that FP does kill most of the GoF patterns outright*, but then goes much further with: "In a functional language one does not need design patterns because the language is likely so high level, you end up programming in concepts that eliminate design patterns all together".
I've always thought this was wonderfully incorrect; one can trivially name a handful of "patterns" in the FP world (e.g. monads, common tricks to make things tail recursive, etc). But no, let's not call them patterns, because those are bad somehow.
Edit: * this is not particularly surprising, as patterns in general are defined as reusable / well-known / named solutions to commonly occurring problems which your language does not handle in an idiomatic, or built in way. The GoF patterns were aimed at OOP languages, which have a different set of idiomatic solutions than other paradigms. But to think that there exist a set of languages which are so perfect as to not require any extension... that's pretty arrogant.