Monadic Operations in Modern CPP
Monadic Operations in Modern CPP
3
Agenda
● Briefly about expected and optional
● Common use cases of expected
● Monadic operations in software development
● Tips and tricks
4
In this talk
● Less theory
● C++ only
● Practical examples
5
Where do examples come from?
6
Our internal framework
… … …
8
Technologies we use
● C++ 20
● vcpkg
● Many 3rd-party libraries (e.g. ranges-v3, tl-expected, catch2 etc)
● Qt for UI on devices
9
Briefly about Qt
C++ code: QML code:
10
Briefly about classes
11
std::optional
#include <optional> ● Contains a value
● …or doesn’t contain a value
● Close to std::pair<T, bool>
std::optional<int> optionalBox;
optionalBox = 42;
fmt::println(
"The value is: {}",
optionalBox.value());
12
std::optional as a return value
When an operation can fail, but it doesn't matter why:
template<class T>
[[nodiscard]]
std::optional<std::size_t> Vector<T>::indexOf(const T &element) const {
./ Do lookup...
return std::nullopt; ./ If not found
}
13
std::optional as a parameter
When you need to pass some auxiliary arguments:
Url resolveUrl(
std::string_view input,
std::optional<Configuration> configuration = std::nullopt)
{
./ Read configuration if any
./ Resolve
}
14
It’ll be iterable
C++ 26
The optional object is a view that contains either one element if it contains a value, or otherwise
zero elements if it does not contain a value. The lifetime of the contained element is bound to the
object.
15
std::expected
#include <expected> ● Either a value
● …or an error
● Close to std::variant<T, E>
std::expected<int, Error> expectedBox;
expectedBox = 42;
fmt::println(
"The value is: {}",
expectedBox.value());
16
std::expected as a return value
When an operation can fail and we need to know why:
./ Actual result
return Widget{ .* ... ./ };
}
17
Where can I get it?
● std::*
● tl::* (via vcpkg or Conan)
18
Use cases
19
What do we use?
● std::expected (approximately 90% of all cases)
● Error handling
● To unify interface
20
Process std::expected
void loadWidget()
{
if (const auto widgetBox = getNewWidget(); widgetBox.has_value()) {
const auto widget = widgetBox.value();
./ Do something with the widget ...
} else {
const auto error = widgetBox.error();
./ Handle the error ...
}
}
21
That was not entirely bad example…
void loadWidgetV2()
{
const auto widgetBox = getNewWidget();
if (widgetBox.has_value()) {
./ Do something with the widget ...
} else {
const auto error = widgetBox.error();
log("Cannot get a new widget {}: {}.", widgetBox.value(), error);
}
}
22
How do we handle this?
23
Monadic operations: and_then
if (const auto widgetBox = getNewWidget(); widgetBox.has_value()) {
const auto widget = widgetBox.value();
./ Do something with the widget ...
}
getWidget().and_then(
[](const auto &widget) :> std::expected<Widget, WidgetError> {
./ Do something with the widget ...
return widget;
});
24
Monadic operations: transform
if (const auto widgetBox = getWidget(); widgetBox.has_value()) {
const auto widget = widgetBox.value();
return widget.id();
}
25
Monadic operations: or_else
if (const auto widgetBox = getWidget(); widgetBox.has_value()) {
./ ...
} else {
log(widgetBox.error());
}
26
Monadic operations: transfrom_error
if (const auto widgetBox = getWidget(); widgetBox.has_value()) {
./ ...
} else {
return fmt::to_string(widgetBox.error());
}
27
On the way to software development
28
Key parts
● People
● Design
● Start with small pieces
29
People
30
An example: widget ID – left or right?
std::expected<ID, WidgetError> loadWidget() std::expected<ID, WidgetError> loadWidget()
{ {
if (const auto widgetBox = getWidget()) { return getWidget()
const auto widget = widgetBox.value(); .* .and_then ... ./
.transform(&Widget::id);
./ Do something with the widget ... }
return widget.id();
} else {
return {};
}
}
31
Observations
● Functional programming is difficult for many
● Combining functions using “monadic operations” is not straightforward
● std::optional and std::expected is terra incognita
32
What should I do about this?
● Show practical benefits (e.g. less boilerplate code)
● Do more workshops
● Do design review
33
Design
34
You will not be doing purely functional programming
● There will be side effects
● There will be object-oriented-style parts of the existing code base
● There will be integration with 3rd-party libraries
● …
35
Try splitting functional-style code from the rest
● A part where we use “monadics” and apply the best practices of functional
programming
36
Where is the boundary?
● Class interface
● Library interface
37
Example: window layout class diagram
IWindowLayout UI::WindowLayout
WindowLayoutStack
38
Example: window layout
class IWindowLayout
{
public:
virtual core::Expected:> add(const window::Uri& windowUri) = 0;
};
./ ...
39
Example: window layout wrapper
./ Inside another namespace
Q_OBJECT
./ ...
private:
std::shared_ptr<IWindowLayout> m_windowLayout;
};
40
Example: window layout wrapper
void WindowLayout::add(const window::Uri& windowUri)
{
m_windowLayout:>add(windowUri).or_else(&printError);
}
41
What did we achieve?
● Integration with the existing codebase
● Monadic interface
● Separation of functional and non-functional code
42
Small steps
43
General approach (assume you have OO-style code-base)
● Start with the implementation of the methods
● Partially change class interface
● Fully change class interface
● (Optionally) Drop the entire class
44
Example: add a window – initial state
std::shared_ptr<Window> loadWindow(
const window::Uri &windowUri, const window::Loader &loader);
./ ...
return false;
}
45
Example: add a window – change implementation
core::Expected<std::shared_ptr<Window:> loadWindow(
const window::Uri &windowUri, const window::Loader &loader);
./ ...
./ ...
return result.has_value();
}
46
Example: add a window – change interface
core::Expected:> WindowLayoutStack::add(const window::Uri& windowUri)
{
return loadWindow(windowUri, m_loader)
.and_then(&addToLayout)
.* .and_then... ./;
}
47
Tips and Tricks
48
Assuming
● You’re at the very beginning of your journey
● There are not many well-defined practices for using monadic operations
● There are not too many functional programming libraries used in the project
49
Lambda functions
● Lambda functions are flexible and powerful tool
● With noisy syntax
● And this is yet another footgun
Use less:
return loadWindow(
windowUri, m_windowLoader)
.and_then(addToLayout)
.and_then(updateActiveWindow)
.or_else(addWindowLayoutPrefix);
}
51
…and more free functions in general
● Small steps with names
● Easy to reuse
● Easy to test
52
Use bind_back/front
● There is already a function
● …used in many places
● …and you cannot easily change a signature
● A solution with lambda functions looks too cumbersome
● std::bind_front – C++20
● std::bind_back – C++23
53
bind_back/front
auto add = [](int a, int b) { return a + b; };
auto addOne = std::bind_back(add, 1);
int a{};
auto incA = std::bind_front(inc, std::ref(a));
incA(2);
fmt::println("{}", a);
54
Possible implementation of bind_back
template<class F, class ::.BoundArgs>
auto bind_back(F :&f, BoundArgs :&::.boundArgs)
{
return [::.boundArgs = std::forward<BoundArgs>(boundArgs), f = f]
<class ::.RemainArgs>(RemainArgs :&::.remainArgs) {
return std::invoke(f, std::forward<RemainArgs>(remainArgs)::., boundArgs::.);
};
}
55
Example of using xostd::bind_back
core::Expected:> addWindowLayoutPrefix(
};
//...
.or_else(std::bind_back(&addWindowLayoutPrefix, "[StackLayout]"));
56
Make all functions return expected/optional
● Easy to compose
● Doesn’t require additional libraries or helpers
return {};
}
57
Tuples your best friends
● No need to create extra structures
● Easy to pass several objects
58
Don’t forget about transform_error
● 3rd-party libraries can have different error types
● Need to pass additional context
● Need to amend existing error (e.g. add a message prefix)
59
Explore 3rd-party libraries
● Get inspired
● Learn
● Use well-tested solutions
60
Thank you!
61