0% found this document useful (0 votes)
30 views61 pages

Monadic Operations in Modern CPP

Uploaded by

Cheng Yuan Chang
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
30 views61 pages

Monadic Operations in Modern CPP

Uploaded by

Cheng Yuan Chang
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 61

Monadic Operations in Modern

C++: A Practical Approach


2
About me
● Vitaly Fanaskov
● Senior software engineer at reMarkable
● 10+ years of C++ experience
● GIS, VFX, frameworks, and libraries
● Ph.D (CS)

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

Library 1 Module 1 Bundle 1

Library 2 Module 2 Bundle 2

Library 3 Module 3 Bundle 3

… … …

Library N Module N Bundle N


7
User interface subsystem
● Collect windows and widgets from modules
● Use bindings to 3rd party libraries (e.g. Qt) to display them
● Navigation

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:

● Business logic ● UI-related things


● Integration
● System-level

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:

std::expected<Widget, WidgetError> loadWidget()


{
./ If error
return std::unexpected(WidgetError{ .* ... ./ });

./ 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();
}

getWidget().transform([](const auto &widget) :> ID { return widget.id(); });

25
Monadic operations: or_else
if (const auto widgetBox = getWidget(); widgetBox.has_value()) {
./ ...
} else {
log(widgetBox.error());
}

getWidget().and_then(.* ... ./).or_else([](const auto& error){ log(error); });

26
Monadic operations: transfrom_error
if (const auto widgetBox = getWidget(); widgetBox.has_value()) {
./ ...
} else {
return fmt::to_string(widgetBox.error());
}

getWidget().and_then(.* ... ./).transform_error(&fmt::to_string<WidgetError>);

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

● A part where it can be difficult to do (or it doesn't make much sense)


○ Final logging output
○ Integration with UI libraries
○ Glue-code
○ ...

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;
};

./ ...

core::Expected:> WindowLayoutStack::add(const window::Uri& windowUri)


{
return loadWindow(windowUri, m_windowLoader)
.and_then(&addToLayout)
.and_then(&updateActiveWindow)
.or_else(&addWindowLayoutPrefix);
}

39
Example: window layout wrapper
./ Inside another namespace

class WindowLayout : public QObject

Q_OBJECT

Q_PROPERTY(QString activeWindow READ activeWindow NOTIFY activeWindowChanged)

./ ...

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);

./ ...

bool WindowLayoutStack::add(const window::Uri& windowUri)


{
if (auto window = loadWindow(windowUri, m_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);

./ ...

bool WindowLayoutStack::add(const window::Uri &windowUri)


{
auto result = loadWindow(windowUri, m_loader)
.and_then(&addToLayout)
.* .and_then... ./;

./ ...

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:

● Nested lambda functions


● Long lambda functions
● Lambda functions assigned to local variables
50
Use less lambda functions
core::Expected:> WindowLayoutStack::add( core::Expected:> WindowLayoutStack::add(
const window::Uri& windowUri) const window::Uri& windowUri)
{ {
return loadWindow( const auto addToLayout =
windowUri, m_windowLoader) []( /**/ ) { /**/ };
.and_then(&addToLayout) const auto updateActiveWindow =
.and_then(&updateActiveWindow) []( /**/ ) { /**/ };
.or_else(&addWindowLayoutPrefix); const auto addWindowLayoutPrefix =
} []( /**/ ) { /**/ };

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);

fmt::println("{}", addOne(2)); ./ prints 3

auto inc = [](int &a, int v) { a += v; };

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(

const std::string &errorString, std::string_view customPrefix)

return core::make_unexpected(fmt::format("{} {}", errorString, customPrefix));

};

//...

.or_else(std::bind_back(&addWindowLayoutPrefix, "[StackLayout]"));

56
Make all functions return expected/optional
● Easy to compose
● Doesn’t require additional libraries or helpers

core::Expected:> removeFromLayout(MapIt windowIterator, Map& windows, Stack& windowsStack)


{
windowsStack.erase(windowIterator:>second);
windows.erase(windowIterator);

return {};
}

57
Tuples your best friends
● No need to create extra structures
● Easy to pass several objects

core::Expected<std::tuple<Context, Component, QString:>


createComponent(QQmlEngine& engine, const QString& fileName, const QString& viewName)
{
./...
return std::make_tuple(std::move(context), std::move(component), viewName);
}

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

You might also like