title: Using Monoids in C++ v1.3
author: Kristoffel Pirard
url: http://github.com/xtofl/articles/monoid/monoid.md
articles> make monoid/monoid.reveal
iota
discussions latelywhetting some appetite for FP
But... what groceries do I need?
Week
after week
after week
can't a computer do that?
I wish I could do...
> python3 growser.py
o currysaus <1 pak>
o chipolata <1 pak>
o basmati <1 kg>
...
shopping_list_menu = resulting_list(all_dishes, pantry)
shopping_list = join_ingredients(shopping_list_menu, extras)
print_ingredients(shopping_list, shop=the_shop)
But look at that code... :(
cart = {}
for dish in menu:
for i in dish.ingredients:
if not i.name in cart:
cart[i.name] = i.amount
else:
assert
cart[i.name].amount.unit == i.amount.unit
cart[i.name].amount.n += i.amount.n
Reading about Category Theory, bumping into terms like
wooow.... scary!
Discussions about 'vacuous truth/falsity'
* all({true, false, true}) == false
* all({true}) == true
* all({}) == ????
</How I got here>
And then something 'clicked'.
cart = {}
for dish in menu:
for i in dish.ingredients:
if not i.name in cart:
cart[i.name] = i.amount
else:
assert
cart[i.name].amount.unit == i.amount.unit
cart[i.name].amount.n += i.amount.n
Imagine using a default-dict here.
And a way to 'add' dicts, key-wise...
cart = sum(dish.ingredients for dish in menu)
How come I didn't think of that sooner?
... dreaming during math class?
The 'math' lingo does not map onto 'programming'.
Lack of application
= lingo mismatch
An attempt to take away some fear and some dismay
=> small steps!, real-life *!
adapted from -
This may look like part of a Category Theory course
but it's not.
But we may need such courses
A Monoid is a tuple <S, op, id>
so that
Monoid: a tuple <S, ⋄, id> so that
* a && b is a boolean
* a && (b && c) == (a && b) && c
* a && true == a, true && a == a
* a || b is a boolean
* a || (b || c) == (a || b) || c
* a || false == a, false || a == a
string string::append(string)
string operator+(string, string)
"ab"s + ("cd"s + "ef"s) == ("ab"s + "cd"s) + "ef"s
"ab"s + ""s == "ab"s == ""s + "ab"s
double operator+(double, double)
.5 + (1. + 2.) == (.5 + 1.) + 2.
.5 + 0. == 0. + .5 == .5
.1 + (.2 + .3) == (.1 + .2) + .3
NOT associative!
Generic functions, of course!
Functions first defined on Ranges can often be generalized to any monoid.
// explicit knowledge
template<typename InputIt, typename T,
typename BinaryOperation>
T accumulate(InputIt b, InputIt e, T init,
BinaryOperation op );
// requires BinaryOperation to satisfy
// T t = op(t, *first)
vs
// implicit knowledge
template<typename InputIt, typename Monoid>
Monoid accumulate(InputIt b, InputIt e);
Divide and Conquer
acc(a, b, c, d) ==
acc(
acc(a, b),
acc(c, d)
)
Theorems for Free!
tuple<int, bool>
under (+, ||)
with id=(0, false)
foo(bar(baz(m))) == mconcat({foo, bar, baz})(m)
But we need translators
Goal:
Define and use mconcat(begin, end) -> M
auto result = mconcat(begin(xs), end(xs));
operator +
and add a 0
constructorLet's take the wrong turn first.
Generic mappend
and mempty
template<typename T> T mempty();
template<typename T> T mappend(T, T);
template<typename Monoid>
auto mconcat(It b, It e) {
return accumulate(
b, e,
mempty<Monoid>(),
mappend<Monoid>);
}
Specialization for int
addition
template<>
int mempty<int>() { return 0; }
template<>
int mappend<int>(int a, int b) { return a + b; }
std::vector<int> ints{{1, 2, 3, 4}};
EXPECT_EQ(10, mconcat(begin(ints), end(ints)));
And for a custom type
struct Custom {
std::string s;
int n;
};
template<>
Custom mempty<Custom>() { return {}; }
template<>
Custom mappend<Custom>(Custom c, Custom d) {
return {
c.s.append(d.s),
c.n + d.n
};
}
For int product:
template<>
int mempty<int>() { return 1; }
template<>
int mappend<int>(int a, int b) { return a * b; }
Can we have 2 specializations for int
?
// error: redefinition of // ‘T overloading::mempty() [with T = int]’
So some extra info is needed
Specify the monoid set, operator and identity.
auto intsum = monoid(0, std::plus<int>{});
All the info is there!
0
is an int
plus<int>
template<typename T_, typename Mappend_t>
struct Monoid {
using T = T_;
T mempty;
Mappend_t mappend;
};
Usage:
Monoid<int, std::plus<int>> intsum{0, std::plus<int>{}};
... what a mess :(
Generic lambda's to the rescue!
auto monoid = [](auto e, auto f) {
return Monoid<decltype(e), decltype(f)>{e, f};
};
Usage:
auto intsum = monoid(0, std::plus<int>);
auto intproduct = monoid(1, std::multiplies<int>);
EXPECT_EQ(10, intsum.mconcat(ints));
EXPECT_EQ(24, intproduct.mconcat(ints));
Cf. also Linear Types/ligthning talk
Define some simple data types:
using Name = std::string;
using Amount = int;
using GroceryList = std::map<Name, Amount>
Example dishes
name | dish1 | dish2 | dish3 |
---|---|---|---|
carrots | 5 | - | 10 |
minced meat | 300g | 300g | - |
rice | 200g | 200g | - |
spaghetti | - | - | 400g |
template<typename It>
GroceryList join_grocerylists(It b, It e) {
static_assert(std::is_same_v<typename It::value_type,
GroceryList>);
GroceryList result{};
for( ; b!=e ; ++b) {
for(const auto &ingredient: b->items) {
result.items[ingredient.first] +=
ingredient.second;
}
}
return result;
}
const auto grocery_monoid = monoid(
GroceryList{},
[](auto a, auto b){
for (const auto &ib: b.items) {
a.items[ib.first] += ib.second;
}
return a;
});
template<typename It>
auto join_grocerylists(It b, It e) {
return mconcat(grocerylist_monoid, b, e);
}
From the treasure trove: Algebraic Data Types composed of Monoids are also Monoids.
A map<K, V>
resembles an 'infinite struct' of values.
So...
Would map<K, Monoid>
also form a Monoid?
Imagine we can 'declare' the monoid within a map
template<typename Map, typename Monoid>
auto fmonoid(Monoid m) {
auto mappend = ...;
return monoid(
Map{},
mappend
};
}
We Can!
// remember, m is a monoid
auto mappend = [=](Map a, Map b) {
for(const auto& kv: b) {
auto &xa = find_or_create(a, kv.first, m.mempty);
auto xb = kv.second;
xa = m.mappend(xa, xb);
}
return a;
}
Don't mind the braces...
EXPECT_EQ(
(IntMap{{{"one", 3}, {"two", 7}, {"three", 13}}}),
mconcat(
fmonoid<IntMap>(monoid(0, std::plus<int>{})),
std::vector<IntMap>({
{{ {"one", 1}, {"two", 4}, {"three", 9} }},
{{ {"one", 2}, {"two", 3}, {"three", 4} }}
})
)
);
const auto intmapmonoid =
lean::fmonoid<std::map<std::string, int>>(
lean::monoid(0, std::plus<int>{})
);
const auto grocery_monoid = lean::monoid(
GroceryList{intmapmonoid.mempty},
[](auto a, auto b) -> GroceryList {
return {intmapmonoid.mappend(a.items, b.items)};
}
);
(We could dig deeper and also extract the { ... .items}
)
template<typename It, typename T>
requires Monoid<T>
auto mconcat(It b, It e) {...}
Some times you have no Unit
max
You can create a 'Sum' type using std::variant
or std::optional
:
In C++... use an optional<T>
// let m be a monoid:
auto tmonoid = monoid(
optional<T>{},
[](auto a, auto b) -> optional<T> {
return {
(a && b)
? {append(*a, *b)}
: a
? a
: b ? b : {} };
);
Ok, done!