The presentation was an introduction to the concepts behind Functional Programming. It went over the pillars of FP, one by one, before concluding with practical advices on how to use this knowledge in our (most probably object-oriented) code base.
Although it was designed as an introduction talk, and even as regular practitioner of Haskell and Clojure, I found quite a lot of things to learn.
What amazed me the most was the way Arnaud was able to summarize complex ideas into simple definitions. He managed to find analogies and make interesting parallels that made these concepts easily understandable.
I really enjoyed it. So I thought it would be a good idea to write down what stuck in my mind, and share it to you in this post.
What is a state? The result of combining a value with time. This is how you get variables and overall, this is a good recipe to get bugs.
The functional paradigm language is about getting rid of this complexity by getting rid of time, and play with immutable values instead. The trick is to avoid variables, and create new values for each “modification” we would do in imperative programming style.
Contrary to what one could expect, creating these new values can be done very efficiently. Persistent data structures are based on structural sharing, the ability to re-use part of the old data value to construct the new one.
Persistent data structures do not make the cost of creating new values disappear entirely. They do consume more memory than their respective counterparts. They also add indirections that may cost in terms of computation time.
But this cost can be easily offset by the whole range of possible optimization they make possible in exchange. In particular:
- Much easier use of parallel or distributed computations
- No need to use defensive locking mechanisms
On (pure) Functions
Pure functions can be seen as delayed values: they just miss some arguments to be able to deliver their value. Give them the same arguments twice, and they output the same value. This has important consequences:
Memoization: Instead of recomputing the values of a pure functions called with the same argument, we can keep the result in store for the next time it is called. Pure functions make caching almost trivial.
Lazy evaluation: We can emulate the lazy evaluation of languages like Haskell by wrapping a computation inside a lambda that takes no arguments. Forcing the computation is just calling that closure with no arguments.
First class functions: If functions are like values, there is no reason you could not manipulate them as values. In particular, it means having functions returning functions, and functions taking functions as parameters, both of which are called higher-order functions.
Objects and Closures
OOP is based on smart data structures, classes that carry with them the functions that operate on them. Functional Programming is based on simpler data structures: functions that operate on the data structure are moved outside of it.
The FP view makes for easier composition of functions, especially if you see the functions as transformation between two sets of values. Chaining functions becomes function composition, and can be done without utilizing side effects.
Arnaud ended this sections with a wonderful comparison of Closure and Objects. He pointed out that Closures can be seen as the inverse of Objects:
- Objects are data that carry some functions with them
- Closures are functions that carry some data with them
On Type Systems
Note: this topic is not one I know a lot, so there might be re-transcription issues.
Functional programming is usually based on structural typing, whereas Object Oriented Programming is usually based on nominative typing.
Structural typing checks if types are equivalent by matching their structure. If two values have a similar structure, they can be considered of the same type.
Nominative typing checks if types are equivalent by matching their “name”. If two values have the name class name, they are considered of the same type.
As an example of structural typing, if a Coordinate is made of two doubles, and if a function translate expects a Coordinate, providing it two doubles will work fine. In the case of nominative typing, this would not type-check: you would need an explicit Coordinate data structure to be instantiated.
This is a trade-off. Structural typing is more loose and flexible than nominative typing. It makes it easier to build generic functions that can apply on different data structures. But it comes at a price: nominative type systems are better at ensuring correctness of your program.
In reality, this dichotomy is not as strict as it may sound. Some languages reinforce the structural typing by putting a name on the structure. This ensures the same correctness guaranty as nominative type systems, but provides a greater expressiveness. This kind of type system is called Algebraic typing.
Pattern matching and Polymorphism
Pattern matching in Functional Programming can be seen as the opposite of polymorphism in Object Oriented Programming:
- Interfaces in OOP are closed under the addition of new functions, but not on a addition of new types. The dispatch is based on the name of the type.
- Algebraic data types are closed under addition of new types, but open in terms of addition of new functions. The dispatch is based on the structure.
We can see there a reference to the Expression Problem coined by Philip Wadler. Both use cases exists: for example in Haskell, you can switch to Typeclasses when you need to be open to new types and closed to new functions.
I guess this is why Arnaud talked about not being a zealot and advised against the strict segregation of paradigms.
The talk ended by a collection of practical advices for all of us that do not program into a “natively functional programming language”.
I think these pieces of advice are especially useful since most of developers do not have access to languages like Haskell or Clojure at their workplace:
- Try to use immutability as much as you can: use const everywhere you possibly can. This will catch a lot of bugs.
- Use algorithms like map, filter and reduce to avoid states in your looping constructs. It makes the code more declarative and less error prone.
- Use Algebraic Data Types. They are available in most languages: JS, Python, std::variant in C++, etc. Use them.
- To handle concurrency, try to rely on concepts like actors to avoid the synchronization nightmares (dead-lock and other performances issues).
- Use concepts like Functional Core, Imperative Shell to make your business logic code as pure as possible.