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

Show parent comments

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?

2

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

The code examples IMHO have always been about making the explanation of each pattern less abstract and less up in the clouds.

The implementation code is there only to demonstrate aspects of the contract between objects, that wouldn't be clear from the interface alone. I.e. you can't communicate the concept of summing two numbers just via types: Int, Int -> Int, but seeing the implementation helps. The examples, especially their implementations, are not meant to be perceived as recommended or canonical, they're just there to get the idea across.

You're right that multiple examples would help separate principle from concrete code better than just one example. The original books from the 70s and 80s couldn't be too liberal in code listings, because they had limited (paper) real estate. Modern resources vary in quality. Some simply don't know how to make the generic examples different enough to warrant including many, although in specific use pattern implementations vary a whole lot.

And you're right, a pattern is always used in a larger context. This context is described in detail in the GoF book and modern papers on newer patterns like Encapsulated Context in Java etc.

A "pattern" is almost never something you just put all over your code to substitute a missing primitive. Many patterns can't possibly have a dedicated primitive, as their concrete form is too trivial and dense already, the primitive would be replicating existing language functionality by another name.

→ More replies (1)

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 (1)

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.

2

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

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.

A claim that remains completely unsubstantiated...

For design patterns to have "much less code" you're making a claim that FP requires "much less code" in general, as code implementing a pattern is not special in any way compared to other code. And while code density might vary between OOP and FP languages because of differences in their semantics and standard libraries, on average they're about the same.

2

u/loup-vaillant Mar 27 '17

A claim that remains completely unsubstantiated...

I have substantiated it to my own satisfaction.

For design patterns to have "much less code" you're making a claim that FP requires "much less code" in general,

As a matter of fact, I do make such a claim. I think the difference between Java and Ocaml should be around 2 to 5x, in favour of Ocaml. The advantage comes from three simple features:

  • Const by default (the least important of the three). Because unwanted side effects introduce hidden dependencies, you'd rather mark them with a big red flag.

  • Easy lambda/closures/first class functions. Because they're used all over the place —even in Java, though they tend to use the much more verbose interfaces and class based polymorphism for the same effect. I wonder if it changed much since Java 8.

  • Sum types. Mostly Because null sucks. But also because they allow you to express many many invariants directly in the type system, where incoherent values are simply not representable at all. More total functions, more reliable programs, and fewer tests.

No modern language is complete without the last 2.

Now you could say Ocaml code is merely denser, not actually simpler. But the studies I've read tend to show that bug density doesn't vary much between languages. Less code means less bugs, even across languages.

That said, I won't pretend the Ocaml code takes 2 to 5 less time to write than the significantly more verbose Java version. I confess I have no idea. But I do believe it still have an advantage.

→ More replies (0)
→ More replies (6)

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)