"harmony" is a header only library for working with monad in the C++ world.
It identifies monadic types by CPO and concept, and adds support for bind and some monadic operations.
A monadic type, for example...
- Pointer
- Smart Pointer (
std::unique_ptr<T>, std::shared_ptr<T>
) std::optional<T>
- Containers (
std::vector<T>, std::list<T>... etc
) Either<L, R>
(Result<T, E>
) like types- Any program defined types that can recognized monad
#include <iostream>
#include <optional>
// Main header of this library
#include "harmony.hpp"
int main() {
std::optional<int> opt = 10;
// Processing chaining
std::optional<int> result = harmony::monas(opt) | [](int n) { return n + n; }
| [](int n) { return n + 100;};
std::cout << *result; // 120
}
#include <iostream>
#include <optional>
#include "harmony.hpp"
int main() {
std::optional<int> opt = 10;
std::optional<int> result = harmony::monas(opt) | [](int n) { return n + n; }
| [](int n) { return n + 100; }
| [](int) { return std::nullopt; } // A processsing that fails
| [](int n) { return n*n; };
if (harmony::validate(result)) {
std::cout << *result;
} else {
std::cout << "failed!"; // This is called
}
}
- Generic library based on Customization Point Object (CPO) and Concept
- All bind operator (
operator|
) is use Hidden friends idiom - Header only
- Requires C++20 or later
- GCC 10.1 or later
- MSVC 2019 Preview latest
The uwrappable
concept determines whether a type is monadic and is a fundamental concept in this library.
It's defined as follows:
template<typename T>
concept unwrappable = requires(T&& m) {
{ harmony::cpo::unwrap(std::forward<T>(m)) } -> not_void;
};
It is required to be able to retrieve the value contained in the type by unwrap
CPO.
The name harmony::unwrap
denotes a customization point object.
Given a subexpression E
with type T
, let t
be an lvalue that denotes the reified object for E
. Then:
- If
T
is an pointer type or indirectly readable (byoperator*
) class type,harmony::unwrap(E)
is expression-equivalent to*t
. - Otherwise, if
t.value()
is a valid expression whose type not void,harmony::unwrap(E)
is expression-equivalent tot.value()
. - Otherwise, if
t.unwrap()
is a valid expression whose type not void,harmony::unwrap(E)
is expression-equivalent tot.unwrap()
. - Otherwise, if
T
modelesstd::ranges::range
,harmony::unwrap(E)
isE
. - Otherwise,
harmony::unwrap(E)
is ill-formed.
If E
is an rvalue, we get the same result as above with t
as the rvalue.
The types that modeled maybe
, list
correspond to maybe monad, list monad, respectively.
template<typename T>
concept maybe =
unwrappable<T> and
requires(const T& m) {
{ harmony::cpo::validate(m) } -> std::same_as<bool>;
};
template<typename T>
concept list = maybe<T> and std::ranges::range<T>;
list
is maybe
and range
, maybe
is uwrappable
and requires that it is possible to determine if the contents are present by validate
CPO.
The name harmony::validate
denotes a customization point object.
Given a subexpression E
with type T
, let t
be an const lvalue that denotes the reified object for E
. Then:
- If
T
not modelesunwrappable
,harmony::validate(E)
is ill-formed. - If
bool(t)
is a valid expression,harmony::validate(E)
is expression-equivalent tobool(t)
. - Otherwise, if
t.has_value()
is a valid expression,harmony::validate(E)
is expression-equivalent tot.has_value()
. - Otherwise, if
t.is_ok()
is a valid expression,harmony::validate(E)
is expression-equivalent tot.is_ok()
. - Otherwise, if
std::ranges::empty(t)
is a valid expression,harmony::validate(E)
is expression-equivalent tostd::ranges::empty(t)
. - Otherwise,
harmony::validate(E)
is ill-formed.
Whenever harmony::validate(E)
is a valid expression, it has type bool.
rewrappable
indicates that the value of type T
can be unit (or return) for an object of type M
.
template<typename M, typename T>
concept rewrappable =
unwrappable<M> and
requires(M& m, T&& v) {
harmony::cpo::unit(m, std::forward<T>(v));
};
This is also defined by unit
CPO.
The name harmony::unit
denotes a customization point object.
Given a subexpression E
and F
with type T
and U
, let t, u
be an lvalue that denotes the reified object for E, F
, let m
that denotes the result for cpo::unwrap(t)
. Then:
- If
T
not modelesunwrappable
,harmony::unit(E, F)
is ill-formed. - Otherwise, If
m
is lvalue reference,decltype((m))
andU
modelsstd::assignable_from
,harmony::unit(E, F)
is expression-equivalent tom = u
. - Otherwise, if
T&
andU
modelsstd::assignable_from
,harmony::unit(E, F)
is expression-equivalent tot = u
. - Otherwise,
harmony::unit(E, F)
is ill-formed.
If F
is an rvalue, we get the same result as above with u
as the rvalue.
monadic
indicates that the result of applying callable F
to the contents of unwrappable M
can be reassigned by unit
CPO.
template<typename F, typename M>
concept monadic =
std::invocable<F, traits::unwrap_t<M>> and
rewrappable<M, std::invoke_result_t<F, traits::unwrap_t<M>>>;
The type that models either
corresponds to Either monad.
template<typename T>
concept either =
maybe<T> and
requires(T&& t) {
{cpo::unwrap_other(std::forward<T>(t))} -> not_void;
};
either
is maybe
, and indicates that an invalid value (equivalent to) can be retrieved.
This is also defined by unwrap_other
CPO.
The name harmony::unwrap_other
denotes a customization point object.
Given a subexpression E
with type T
, let t
be an lvalue that denotes the reified object for E
. Then:
- If
T
not modelesmaybe
,harmony::unwrap_other(E)
is ill-formed. - If
T
is an specialization ofstd::optional
,harmony::unwrap_other(E)
isstd::nullopt
. - Otherwise, if
T
is an pointer type or pointer like type (e.g smart pointer types),harmony::unwrap_other(E)
isnullptr
. - Otherwise, if
t.error()
is a valid expression whose type not void,harmony::unwrap_other(E)
is expression-equivalent tot.error()
. - Otherwise, if
t.unwrap_err()
is a valid expression whose type not void,harmony::unwrap_other(E)
is expression-equivalent tot.unwrap_err()
. - Otherwise,
harmony::unwrap_other(E)
is ill-formed.
If E
is an rvalue, we get the same result as above with t
as the rvalue.
harmony::monas
is the starting point for using the facilities of this library. It's a thin wrapper for monadic types.
This library uses operator|
as the bind operator (e.g >>=
).
#include <iostream>
#include <optional>
// Main header of this library
#include "harmony.hpp"
int main() {
std::optional<int> opt = 10;
// Process chaining
std::optional<int> result = harmony::monas(opt) | [](int n) { return n + n; }
| [](int n) { return n + 100;};
std::cout << *result; // 120
}
(The code at the beginning is republished.)
You can chain any number of operations on valid values. They will not be called on invalid values.
However, if you want to change the type, use map
.
map/transform
performs the conversion of valid values and map_err
performs the conversion of invalid values.
transform
is a mere alias for map
.
int main() {
using namespace harmony::monadic_op;
// Conversion of valid value. int -> double
auto result = std::optional<int>{10} | map([](int n) { return double(n) + 0.1; });
// decltype(result) is not std::optional<double>, but a type like Either<double, nullopt_t>.
std::cout << harmony::unwrap(result) << std::endl; // 10.1
// Conversion of invalid value. std::nullopt_t -> bool
auto err = std::optional<int>{} | map_err([](std::nullopt_t) { return false; });
// decltype(err) is not std::optional<bool>, but a type like Either<int, bool>.
std::cout << std::boolalpha << harmony::unwrap_other(err); // false
}
Both take one Callable object f
, as an argument. The return type of f
is arbitrary, but the result is wrapped in harmony::monas
(So you can continue to chain bind and other monadic operations.).
The type on the left side of | map(...)
must models either
.
and_then
and or_else
are similar to map
and map_err
. The difference is that the Callable return type that you receive must be modeles either
.
The return type in both cases must be able to accept the other unconverted value as is.
int main() {
using namespace harmony::monadic_op;
// Conversion of valid value. int -> double
auto andthen = std::optional<int>{10} | and_then([](int n) { return std::optional<double>(double(n) + 0.1); });
// decltype(*andthen) is std::optional<double>.
std::cout << harmony::unwrap(andthen) << std::endl; // 10.1
// Conversion of invalid value. std::nullopt_t -> bool
auto orelse = std::optional<int>{} | or_else([](std::nullopt_t) { return std::optional<double>(-0.0); });
// decltype(*orelse) is std::optional<double>.
std::cout << harmony::unwrap(orelse); // -0.0
}
These also wrap either
return type with harmony::monas
(So you can continue to chain bind and other monadic operations.).
The type on the left side of | and_then(...)
(| or_else(...)
) must models either
.
match
takes a process for each valid and invalid value and applies it appropriately, depending on the state of the object.
However, the return type must be aggregated into one type.
fold
is a mere alias for match
.
int main() {
using namespace harmony::monadic_op;
int n = 10;
int r = harmony::monas(&n)
| match([](int n){ return 2*n;}, // Processing for valid values
[](std::nullptr_t) { return 0;}); // Processing for invalid values
std::cout << r << std::endl; // 20
int *p = nullptr;
r = harmony::monas(p)
| match([](int){ return 0;}, [](std::nullptr_t) { return 1;});
std::cout << r << std::endl; // 1
}
You can also pass only one Callable object to match
(e.g generic lambda).
The type on the left side of | match(...)
must models either
.
exists
applies the predicate and returns the result if the target object has a valid value. If it has an invalid value, it immediately returns false.
int n = 10;
bool r = harmony::monas(&n)
| [](int n) { return n + n; }
| exists([](int n) { return n == 20;});
// r == true
It also behaves like list
for std::any_of
.
std::vector<int> vec = {2, 4, 6, 8, 10};
bool r = vec | exists([](int n) { return n == 8; });
// r == true
The type on the left side of | exists(...)
must models maybe
.
try_catch
takes a callable f
and its arguments and returns Either with its result and the std::exception_ptr
.
int main() {
using namespace harmony::monadic_op;
// Processing that can throw an exception
auto f = [](int n, int m) -> int {
if (m == 0) throw "division by zero";
return n / m;
};
auto r = try_catch(f, 4, 2)
| map([](int n) { return n == 2; });
std::cout << std::boolalpha << harmony::unwrap(r) << std::endl; // true
auto str = try_catch(f, 4, 0)
| map_err([](std::exception_ptr exptr) {
try { std::rethrow_exception(exptr); }
catch(const char* message) {
return std::string{message};
}
});
std::cout << harmony::unwrap_other(str); // division by zero
}
map_to<T>
and fold_to<T>
are map
and fold(match)
convenience operations, respectively. They return the converted value directly.
Using this, the previous code can be written as follows
int main() {
using namespace harmony::monadic_op;
// Processing that can throw an exception
auto f = [](int n, int m) -> int {
if (m == 0) throw "division by zero";
return n / m;
};
bool r = try_catch(f, 4, 2)
| map([](int n) { return n == 2; })
| map_to<bool>;
std::cout << std::boolalpha << r << std::endl; // true
std::string str = try_catch(f, 4, 0)
| map([](int) { return std::string{}; }) // To match the type
| map_err([](std::exception_ptr exptr) {
try { std::rethrow_exception(exptr); }
catch(const char* message) {
return std::string{message};
}
})
| fold_to<std::string>;
std::cout << str; // division by zero
}
If a type that is merely a maybe
has an invalid value, it returns the default constructed value (if possible).
Also, in both cases, narrowing conversion is not allowed.