Harmony
C++ Monadologie
Install / Use
/learn @onihusube/HarmonyREADME
harmony
"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
Example
#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
}
}
Overview
- 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
Facility
concept uwrappable
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.
CPO unwrap
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
Tis 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
Tmodelesstd::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.
concept maybe list
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.
CPO validate
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
Tnot 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.
concept rewrappable
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.
CPO unit
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
Tnot modelesunwrappable,harmony::unit(E, F)is ill-formed. - Otherwise, If
mis lvalue reference,decltype((m))andUmodelsstd::assignable_from,harmony::unit(E, F)is expression-equivalent tom = u. - Otherwise, if
T&andUmodelsstd::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.
concept monadic
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>>>;
concept either
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.
CPO unwrap_other
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
Tnot modelesmaybe,harmony::unwrap_other(E)is ill-formed. - If
Tis an specialization ofstd::optional,harmony::unwrap_other(E)isstd::nullopt. - Otherwise, if
Tis 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.
type monas<T>
harmony::monas is the starting point for using the facilities of this library. It's a thin wrapper for monadic types.
operator bind
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.
monadic operation map(transform)/map_err
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.
monadic operation and_then/or_else
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
