A talk from CppEurope 2019 about functional programming in C++.
It talks about lambdas, immutability, operations with functions (partial application, currying, functional composition), shows an example and ends with a procedure for refactoring legacy code.
7. About This Talk
• A practical approach to use functional programming in C++
• Focused on essentials and the mindset
• Not purist
• No maths
• Not touching on performance
• My Goal: You should find at least one technique you can use in your
code
11. Why Immutability?
The more obstacles one places in the translation between code and
behavior, the more mistakes (aka bugs) we can make and the least effective
we are as programmers.
12. Example of Mutable Code
How many implementation options exist?
int add(int& first, int& second){
...
}
16. What’s the Semantics?
int add(int first, int second)
int add(int first, int& second)
int add(int& first, int second)
int add(int& first, int& second)
int add(int first, int* second)
int add(int& first, int* second)
int add(int* first, int* second)
int add(int* first, int second)
int add(int* first, int& second)
17. Immutable Code
// If standalone function
int add(const int& first, const int& second)
// If part of a class
int add(const int& first, const int& second) const
How many implementation options?
18. Immutable Code
// If standalone function
int add(const int& first, const int& second){
return first + second;
}
// If part of a class
int add(const int& first, const int& second) const{
return first + second;
}
21. Not a New Idea In C++
• Function pointers
• Functors
22. Let’s Represent Functions As Data
// Lambda variable
auto add = [](int first, int second){
return first + second;
};
More details: http://en.cppreference.com/w/cpp/language/lambda
23. Careful: Lambdas can be mutable
// Mutable
auto increment = [](int& value){return ++value};
int aValue = 42;
int incremented = increment(42);
assert(incremented == 43);
assert(aValue == 43)
24. Careful: Immutable Lambda
• Function call operator is const
• Parameters use their own specifier
// Immutable
auto incrementImmutable = [](const int& value){
return value + 1;
};
int aValue = 42;
int incremented = increment(42);
assert(incremented == 43);
assert(aValue == 42)
25. Type Inference
// Does not compile
auto add(auto first, auto second){
return first + second;
}
// Works for any two values that have operator+ defined
auto add = [](const auto& first, const auto& second){
return first + second;
};
26. Value Capture
Lambdas can capture values from the context.
TEST_CASE(”type inference with lambdas”){
int first = 5;
auto add = [=](const auto& second){
return first + second;
};
CHECK_EQ(42, add(37));
CHECK_EQ(42.5, add(37.5));
}
30. What is an Operation on Functions?
Any operation that takes one (or more functions) and returns a new function.
• Partial Application
• Functional Composition
• Currying
• Higher level functions
31. Partial Application
A function with n-1 parameters is obtained from a function with n
parameters by binding one parameter to a value.
using namespace std::placeholders;
auto divide = [](const auto& first, const auto& second){
return first/second;
};
int specialValue = 10;
auto divideSpecialValue = bind(divide, specialValue, _1);
// Equivalent with
// auto divideSpecialValue = [](const auto& second) {
// return 10 / second
// };
32. Functional Composition
Create a function h(x) from two functions f(y) and g(z), such that for any
value of x, h(x) = f(g(x)).
C++ doesn’t yet have a functional composition operator, so we have to write
our own function:
template<typename F, typename G>
auto compose(F f, G g){
return [f, g](auto x){
return f(g(x));
};
}
33. Example
auto increment = [](const auto& value){
return value + 1;
};
auto incrementTwice = compose(increment, increment);
/* Equivalent with:
auto incrementTwice = [&](const auto& value){
return increment(increment(value));
};
*/
CHECK_EQ(3, incrementTwice(1));
34. Currying
Decompose a function with N arguments into N functions with one argument
auto curriedAdd = [](const auto& first){
return [first](const auto& second){
return first + second;
};
};
35. Currying in C++
Unfortunately, there’s no operator that supports currying.
In pure functional languages, curry is done by default, and linked with
partial application. For example:
auto divide = [](const auto& first, const auto& second){
return first/second;
};
divide(5) => a new function that takes second as parameter, equ
38. Short list of higher level functions
Find them in or
• find_if
• transform
• reduce / accumulate
• count_if
• all_of / any_of / none_of
• …
See examples on https://github.com/MozaicWorks/functionalCpp
40. Conclusions
We can create functions from other functions through partial application,
composition, currying.
We can understand better the functions we write due to immutability.
42. Problem: TicTacToe Result
Given a TicTacToe board that’s either empty or already has moves, print out
the result of the game if the game ended or that the game is still in progress.
43. Approach
1. Clearly define the input; give examples
2. Clearly define the output; give examples
3. Identify a chain of functional transformations you can apply on the
input data to turn it into the output data
44. Clearly Define the Output
• Game not started
• Game in progress
• X won
• O won
• Draw
45. Clearly Define the Input
Empty Board:
_ _ _
_ _ _
_ _ _
X won by line:
X X X
O O _
_ _ _
etc.
46. Turns out that …
X wins if
• any line is filled with X OR
• any column is filled with X OR
• the main diagonal is filled with X OR
• the secondary diagonal is filled with X
47. Transformations
board ->
collection(all lines, all columns, all diagonals) ->
if any(collection, filledWithX) ->
X won
where
filledWithX(line|column|diagonal L) =
all(token on L equals 'X')
48. Remove duplication with functional operators
• Functions with the same parameter? Partial application! Or classes!
• Functions with the same structure? Higher level functions!
• Chained calls? Function composition!
51. Refactoring code through Immutability
Premise:
• Any program can be written as Input -> Pure Functions -> State change
(either UI or storage)
• This property of the code applies on multiple levels, even on a single
function
52. Refactoring Method
• Take a complex piece of code:
• extract a small piece of it as another method
• extract everything mutable as parameter
• until the method is immutable
• check by making everything const and compiling
• then decompose the long immutable function into smaller immutable
functions
• then recombine the functions into classes, based on the parameters
they use
53. We End Up With Functional OOP
• Data structures with set / get or public data
• “Processor” objects that receive data and return other data
• “Run and forget” objects: initialize, execute operations, throw it away
• Separate, pluggable, input / output objects
54. Hypothesis
Unconfirmed Hypothesis: this type of refactoring is safe even without tests.
(Or, we can easily generate tests for the immutable function with
property-based testing)