I remember functional programming being a niche aspect of computing science. Software engineering didn’t consider it at all. Now you can find discussion and books about using it in any major programming language. I read Functional Programming in C# but, to me, this oversold the technique while it lacking practical advice for day to day programming. I had a much better time with Functional Programming in C++ which concentrates a lot more on how to get things done. It tells you about a new tool and how to add it to your toolbox.
Functional programming
Unsurprisingly this paradigm is all about functions, specifically pure functions. These are functions where their output is entirely dependant on their input and they have no side-effects. That means they don’t depend on any global state and they don’t change any global state. This makes them easier to reason about, less likely to cause or be effected by bugs, easier to test and debug, well-suited to parallelisation. I tend to think of functional techniques as force multipliers, they let you get more for less.
As well as using functions this paradigm creates and manipulates functions. So you will be passing functions into functions and / or getting functions back as a result. In C++ this means you’ll be getting into template meta-programming. This is a powerful technique but is also confusing. You have to learn how to use a bunch of build time equivalents to runtime code. Expect build time error messages to be less helpful than you’re use to. On the other hand this book gives some good examples to learn from and reason to do so.
Procedural to functional
The book starts with a good example of taking some simple code using the procedural paradigm and converting it to a functional one. Let’s take a list of filenames and turn it into a list of line counts. I’ll paraphrase it here:
std::vector<uint32_t> CountLinesInFiles(const std::vector<std::string>& filenames) { std::vector<uint32_t> lineCounts; char character = 0; for (const auto& filename : filenames) { uint32_t lineCount = 0; std::ifstream stream(filename); while (stream.get(character)) { if (character == '\n') { lineCount++; } } lineCounts.push_back(lineCount); } return lineCounts; }
This code does the job although I’d have probably broken it into two functions from the start.
uint32_t CountLinesInFile(const std::string& filename) { std::ifstream stream(filename); return std::count(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>(), '\n'); } std::vector<uint32_t> CountLinesInFiles(const std::vector<std::string>& filenames) { std::vector<uint32_t> lineCounts; for (const auto& filename : filenames) { lineCounts.push_back(CountLinesInFile(filename)); } return lineCounts; }
That what the book does next. It’s also making use standard library to do some of the work. Despite the extra function it’s taking up less space.
std::vector<uint32_t> CountLinesInFiles(const std::vector<std::string>& filenames) { std::vector<uint32_t> lineCounts(filenames.size()); std::transform(filenames.cbegin(), filenames.cend(), lineCounts.begin(), CountLinesInFile); return lineCounts; }
More standard library work to transform a vector of one things into a vector of another things.
std::vector<uint32_t> CountLinesInFiles(const std::vector<std::string>& filenames) { return filenames | transform(CountLinesInFile); }
The book introduces the pipe operator to take a collection and push it through a transform.
The book came out before C++20 which includes std::ranges
and
the pipe operator so there may be some discrepancies.
std::vector<uint32_t> CountLinesInFiles(const std::vector<std::string>& filenames) { return filenames | transform(OpenFile) | transform(CountLines); }
Finally we break up CountLinesInFile
to two function concepts OpenFile
and CountLines
,
not included, both of which could be used in other contexts.
All of this sounds good to me and has gone towards my options on repeating code. Whether you can take all your code and do something like this seems less important to me. I think there is definitely some code that could be improved like this.
Functional in C++
After this the book the books takes you through some functional programming fundamentals:
- Fold – A way to convert lists into individual values, e.g. sum.
- Filter – A way to select values from a list.
- Transform – A way to convert values in a list into new values.
- Partial application – Taking a function with some parameters and producing a new function with fewer parameters.
- Curry – Takes a function with some parameters and creates a series of functions with one parameter. The book isn’t overly clear as to why you’d do this.
- Lift – Take a function and create a new function applicable in a broader context, e.g. turn
to_upper
for characters into one for strings.
And how to take those fundamentals and make use of them in C++:
- Lambdas – An unnamed functions that can be passed into a fold, filter or transform.
- Function objects – Custom classes that reduce boilerplate further.
std::bind
or lambdas – Both can be used for partial application.- Template meta-programming – Lets one algorithm work with different types in a controlled way.
Lessons in const
Functional programming uses pure functions that don’t have side effects.
That means you can be using const
all over the place, that guarantees you’re doing it right.
However the book does point out that const variable cannot undergo named return value optimisation
because they cannot use move operations.
It’s a rare case of where const
might hurt.
Building useful tools
The book includes a number of classes and ideas you might want to include in your next project:
LazyValue
– Delays the calculation of a value until it is actually needed.- Memoization – Caches function results so they only have to be calculated once.
LazyStringConcatination
– Lazy string builder implementation.
What are monads?
Monads are where things start to get a bit abstract. I think this eventually happens with a lot of functional programming material. Maybe because it’s been an academic area longer than it’s been used in more practical fields.
They have multiple uses but one seems to be dealing with errors. Lots of function can only sometimes produce a true result. Other times they produce no value or an error value. This can make chains of function increasingly complicated. Monads can strip these extras off, unwrapping things back to the underlying values.
In the end
I found this an interesting and fairly clear read, monads aside. In particular it got me to improve my abilities with templates. I’m sure that pieces of a functional approach can improve a C++ codebase and this book give you a clear idea on how you might do that.
Leave a Reply