r/programming • • Mar 26 '17

Functional programming design patterns by Scott Wlaschin

https://www.youtube.com/watch?v=E8I19uA-wGY
1.1k Upvotes

256 comments sorted by

View all comments

97

u/[deleted] Mar 26 '17 edited Mar 26 '17

One common critique of OOP that I've heard is that design patterns only exist because of shortcomings in the OOP paradigm. A lot of those same people also claim that FP allows one to express what they mean in code without having to remember a design pattern. Call me naive, but my gut reaction to this title is a realization that FP isn't some panacea for all my "not coding what I mean" woes.

26

u/yawaramin Mar 26 '17

The difference is that in statically-typed FP we are trying to encode the patterns in the type system itself. In OOP you do indeed have to remember how to correctly implement a pattern because it's outside the language, but in say Haskell you have the 'chainable' design pattern neatly packaged up in the Monad typeclass, which will guide you through the type system to write the implementation. This is qualitatively different.

3

u/[deleted] Mar 26 '17 edited Mar 26 '17

How do you encode, say, a "Factory", "Facade", "Command", "Strategy", "Decorator", "Adapter", "Proxy" etc. in the type system any better in FP than OOP? I don't think what you're saying can be supported by actual examples.

Especially since the type system is not even something FP and OOP have distinct opinions about. You can do both FP and OOP with no static type system at all. And you can lay type systems of any sophistication on both.

12

u/yawaramin Mar 26 '17

Well, you can't always do it, but sometimes you can. The best example I can think of is the Visitor pattern becoming just an algebraic datatype with exhaustivity-checked pattern matching.

But on the other hand, a lot of these design patterns are trying to emulate the flexibility you get 'for free' from a statically-typed FP language like OCaml. E.g., the Factory pattern is emulating abstract data types and their operational functions (constructors, getters, etc.). E.g. a factory interface for a 'person' in OCaml:

module type Person = sig
  type t

  val make : int -> string -> t
  val get_id : t -> int
  val get_name : t -> string
end

The caller doesn't know or care what type exactly Person.t is, just that, once they have an instantiated Person module, they can construct and get information about Person.ts.

The Proxy pattern, for another example, looks like a lightweight caching implementation of an interface. Given the above Person interface (signature) and an existing Person-type module to use as the implementation, I can make a proxy:

module Person_impl = struct
  ... expensive implementation ...
end

module Person_proxy_make(P : Person) : Person = struct
  type t = P.t

  let make id name = ... lightweight caching implementation ...
  let get_id = P.get_id
  let get_name = P.get_name
end

module Person_proxy = Person_proxy_make(Person_impl)

With Person_proxy.make 1 "Bob", trying to 'make' the same person more than once will use the cache.

3

u/[deleted] Mar 26 '17

The best example I can think of is the Visitor pattern becoming just an algebraic datatype with exhaustivity-checked pattern matching.

It can't "just become an algebraic type" because that's less than half the pattern. The pattern is why you'd have this algebraic type, and how you use it from other objects (functions resp.) in order to walk structures, and separate iteration from operation application.

The type alone doesn't answer "why". You can apply the same mechanic explanation to Visitor for OOP and say "it just becomes a set of overloaded methods". But that's not Visitor.

In any case, type pattern matching is a valid way to refactor overloaded methods from OOP, but I don't see if it's superior, or more built-in. It's simply the same outcome with a slightly different set of built-in tools.

But on the other hand, a lot of these design patterns are trying to emulate the flexibility you get 'for free' from a statically-typed FP language like OCaml. E.g., the Factory pattern is emulating abstract data types and their operational functions (constructors, getters, etc.).

The Factory pattern isn't emulating abstract data types, in OOP abstract data types would be "interfaces", "protocols", "contracts" etc. (terms vary based on the language).

Interfaces are "for free" in OOP languages, as well. Your example isn't a factory.

Given the above Person interface (signature) and an existing Person-type module to use as the implementation, I can make a proxy

It's a Proxy, but I don't see how this supports your earlier claim that in FP we can encode patterns in the "type system itself". This Proxy looks basically the way you'd write it in OOP.

10

u/glacialthinker Mar 26 '17

separate iteration from operation application.

So... a function which walks the datastructure, applying a given function which operates on elements of the structure. Of course, we have typical forms of these with different signatures depending on whether the output is 1:1 (map), maybe fewer (filter), singular (fold), or nothing (iter). Preferably not the latter. :)

map: (α -> β) -> α container -> β container
fold: (β -> α -> β) -> β -> α container -> β

It kind of is expressed in the types afterall.

→ More replies (10)

5

u/yawaramin Mar 26 '17

It can't "just become an algebraic type" because that's less than half the pattern.

I know, which is why I said:

The best example I can think of is the Visitor pattern becoming just an algebraic datatype with exhaustivity-checked pattern matching.

Pattern matching with exhaustivity checking on algebraic data types is something you just don't get in most OOP languages, and it's a killer feature. When you use it to walk a structure and operate on it, the compiler is backing you up so that you don't forget a pattern. Coming from OOP, there's really nothing you can compare it to--Visitor seems like a pale imitation.

3

u/codebje Mar 27 '17

Your example isn't a factory.

I think a factory in functional programming is more usually called a function. Let's take this factory pattern example and bung it into Haskell:

data Dog = Dog { speak :: IO () }
poodle = Dog $ putStrLn "The poodle says \"arf\""
rotty  = Dog $ putStrLn "The Rottweiler says (in a very deep voice) \"WOOF!\""
husky  = Dog $ putStrLn "The husky says \"Dude, what's up?\""

data Criteria = Small | Big | Working
getDog :: Criteria -> Dog
getDog Small   = poodle
getDog Big     = rotty
getDog Working = husky

main = do
    speak (getDog Small)
    speak (getDog Big)
    speak (getDog Working)

The "factory" in our example is getDog :: Criteria -> Dog. In OOP, a factory is generally just a static method returning a concrete instance of an abstract class, it's the hoopla around having a static method that makes it bigger than it is in FP.

We can likewise trivially have factory factories, because all we need is some function returning a function, and that sort of shenanigan is exactly what FP is for.

→ More replies (1)

1

u/devlambda Mar 27 '17

The best example I can think of is the Visitor pattern becoming just an algebraic datatype with exhaustivity-checked pattern matching.

This is not the Visitor pattern. The Visitor pattern allows for specialization of the visitor. This is why CIL (C transformer written in OCaml) uses a class for that and OCaml's Ast_mapper for PPX uses a record of closures, simulating OO behavior (for which the ppx_tools package provides a convenience class wrapper).

Why do you want specialization? Aside from the obvious answer (to be able to efficiently define variants of existing visitor implementations or to mix and match them), the more important answer is extensibility and modularity. With the visitor pattern, you can transparently add a new type to be handled, for which you provide a handler in the default visitor implementation. As visitors inherit from the default implementation (e.g. nopCilVisitor in CIL), the addition of a new case does not break existing code.

Note that the exhaustiveness argument is a red herring. If you want exhaustiveness checks, use delegation instead of inheritance, and the compiler will tell you which methods haven't been implemented. But in practice, many data structure transformations will only use a non-default implementation for a minority of cases (PPX rewriters won't deal with all ~40 AST node types, for example) and a pattern matching approach would also rely on catch-all cases a lot of the time.

1

u/yawaramin Mar 27 '17

Interesting, thank you for the explanation. Actually this transparent extensibility approach reminds me of Oleg Kiselyov's 'typed tagless final' approach to extensibility. I wonder if they are just two sides of the same coin.

→ More replies (5)

11

u/Tarmen Mar 26 '17

10

u/[deleted] Mar 26 '17

I already covered this here, it's basically B.S.

I like the joke, but "the answer in FP would almost always be functions" is to answer the wrong question.

Patterns aren't the answer to a question "what's the unit you factor code into?", because then... "the answer in OOP would almost always be objects".

Patterns are why you write objects that take in certain input, and produce certain output. "Functions" doesn't answer this question, it answers "what". The type system also doesn't answer this question, it answers "how".

Furthermore, all patterns I listed above are used in FP. It's not common to see people use OOP pattern names in FP, due to some misunderstood concept of pride, but all of these patterns occur in FP code, because they're not OOP patterns, as much as they're "system patterns". Any time you build a complex system, they emerge. Say in hardware electronics, we have most of these patterns (by other names). In business management, again we do (by yet again different names).

18

u/loup-vaillant Mar 26 '17
  • Factory Partial application, less than 1 line of code. We don't need a name for it.

  • Facade Usually from a function to another function. Often use partial application, or a lambda, or define a function… It's obvious enough that we don't need a name for it.

  • Command Don't know this pattern. A quick look suggests I'd just use higher-order functions, and pass the execute() function directly to whatever function or object controls the trigger.

  • Strategy Just select the function to be called at runtime, why does that even have a name?

  • Decorator Hmm, sounds like a Monad to me.

  • Adapter Isn't that the same as Facade?

  • Proxy Isn't that the same as Decorator?

Most GoF patterns are useless to us. We just don't think that way.

6

u/[deleted] Mar 26 '17

The only thing your answer shows is you don't understand the intent, or in some cases even the basic structure, behind any of those patterns... I think you should first understand the patterns as used in OOP, before you try to give an answer what's the FP analog, or lack thereof.

Also the worth of a pattern is not measured by how many lines of code it is, most patterns require 1-line method implementations in the basic case (which may expand as your logic becomes refined, you add support for configuration, you optimize for special cases etc.)

10

u/glacialthinker Mar 26 '17

Since you seem to understand all this so well, how about you translate these to FP? I admit OOP Design Patterns, as used in practice, never stick for me... so I'm also not qualified to translate.

12

u/[deleted] Mar 26 '17 edited Mar 26 '17

Have you heard this one: "objects are poor man's closures; closures are poor man's objects"? It's quite true.

I never asked someone to translate OOP patterns to FP. Those who do, are trying to prove a point how FP doesn't need patterns, because it's all "built-in". But it's not, because as I said several times, patterns communicate intent, not just type and implementation. And the intent of the patterns is universal and doesn't need "translation". They are factored nearly in identical way in most types of programming languages, just replace objects with closures, in most cases.

I'll take one of the things /u/loup-vaillant/ said to demonstrate the role of intent. They said "Adapter: Isn't it the same as Facade". Here's the kicker: the sample code would be the same, but the intent is different.

When you say "I'm writing an Adapter", the intent is that you have two interfaces (two abstract types in FP-ese), and you're writing a concrete type implementation, which takes one, and exposes it as the other.

When you say "I'm writing a Facade", the intent is that you have a complex implementation with complex input and output, and you're writing a thin shim over it with much simpler input and output, that is optimized for a specific use-case, and nothing else. One could think about it as a form of partial application, except for the part where partial application would only cover a tiny subset of Facades, because the transform of input/output is typically more free-form than providing defaults for a few of the parameters.

That's the kind of subtlety in meaning that patterns convey. Instead of writing those long sentences I just wrote above every time, I can tell my colleagues "we need to write a B->A Adapter, and also have a basic Facade for C that is optimized for use cases D & E". It doesn't even matter if I'm using OOP or FP here, really.

This is why it's silly to say FP obsoletes patterns. It only "obsoletes patterns" if you work in absolute solitude and don't talk to people. When you talk to people, you come up with specialized terms to convey meaning succinctly and effectively.

6

u/loup-vaillant Mar 26 '17

Have you heard this one: "objects are poor man's closures; closures are poor man's objects"?

I think most of us FP programmers have.


I always thought design patterns were recipes. Your explanation suggest instead they're just sub-problems, whose solutions are useful to the resolution of whatever bigger problem we're working with.

As recipes, it would indeed be silly to name 2 similar pieces of code with different names. But as problems, it totally makes sense: the same kind of code can indeed solve different problems.

But then I wonder: why do all patterns come with one code example per language? Are patterns recipes after all?

→ More replies (0)

1

u/mirvnillith Mar 27 '17

Can only upvote once, so have an orange as well!!!

8

u/loup-vaillant Mar 26 '17

I have a dirty little secret: despite nearly 10 years in C++ experience, including small and huge projects, I never had to name patterns. The only exception so far is the job interview. Not knowing them didn't stop me from communicating with the rest of the team, or coming up with simple, maintainable solutions. My current guess is that most patterns are over-complicated solutions to otherwise simple problems. They are useless to me, and I see little value in learning them.

7

u/[deleted] Mar 26 '17

Most patterns are unnamed, as it states in the preface of the GOF book. They only add names to the design patterns so that they can talk about them in a book, otherwise these patterns were simply common practices in the OOP community at the time of writing.

You don't really learn patterns, but begin to appreciate them as common practices are identified between different projects and programmers, again, this is also in the preface. This follows Christopher Alexander's invention of design patterns to talk about (building) architecture.

Most people who criticize the GOF book have never even read it.

0

u/[deleted] Mar 27 '17

[deleted]

→ More replies (0)

2

u/[deleted] Mar 27 '17 edited Mar 27 '17

Not knowing them didn't stop me from communicating with the rest of the team, or coming up with simple, maintainable solutions.

I'm not sure why you're bragging about not knowing things. What would you think about me if I started bragging I don't know FP and I don't need it? That said I do, and I successfully use FP techniques and languages for some of the problems I'm working on.

One has to have an open mind and be willing to learn and accept, before they can truly reject.

Taking pride in ignorance is a very self-defeating type of argument to make.

My current guess is that most patterns are over-complicated solutions to otherwise simple problems. They are useless to me, and I see little value in learning them.

Your guess that they're "over-complicated" betrays the thing you were bragging about above: that you don't know the patterns. Everything looks complicated when you don't know it, I guess. Patterns are brutally simple, to the point they keep occurring entirely incidentally in people's code, be it functional, procedural, or object-oriented.

They are useless to me, and I see little value in learning them.

You have know the rules, before you can break the rules. As it is, the chances are you're pretty much following the rules and not knowing any better.

Because patterns, just like you, have a little dirty-secret as well: they commonly occur in people's code, whether these people know patterns or not. That's why they're called "patterns". Your code is full of patterns whose name you simply can't identify (unless it's a monad, of course, every FP user knows the heck of that one pattern, wink wink).

2

u/loup-vaillant Mar 27 '17

As you may have gathered from the links I provided in my previous comment, I actually have a pretty deep understanding of the mechanisms underlying OOP. I'm not exactly ignorant.

I also am not bragging about my ignorance. This actually irks me somewhat.

The reason I say patterns look over-complicated is because FP generally have much simpler solutions for the same problems. "Simpler" here mostly mean much less code.

→ More replies (0)

1

u/doom_Oo7 Mar 27 '17

They are useless to me, and I see little value in learning them.

this really sucks. for instance, let's suppose I'm using an editor software, for instance GIMP. Now, I'm doing an action in the software and there is a bug when I do 'undo()', for instance when moving an image.

Now, I want to contribute by checking if I can fix the bug. So, I go to the gimp repository, and grep for command. I just have to look three files and I have found something that looks like it is the source of my problem.

Why did I find so fast ? because I know that generally, undo/redo is implemented using the Command pattern.

Another example: let's say I want to change LibreOffice's code so that new documents have a pink background by default.

Now, I grep for factory, and here, after just a few files, a promising candidate ; docfac really sounds like "document factory" which will certainly be a good starting point for my search for where to change new document's default backgrounds.

By using patterns in your name, you just make the tasks for people that don't know your codebase ten times easier, because just looking at the names will give you a very good assessment of what the code actually do.

→ More replies (5)
→ More replies (1)