I've implemented a simple Maybe type around std::function that implements function composition where any function in the composition can fail (causing the entire composition to fail) - in essence, a Maybe monad where operator<< implements bind.
For example,
// one binary function that cannot fail.
std::function<int(float, float)> h =
[](const float a, const float b) -> int {
return a * b;
};
// a unary function that CAN fail.
std::function<std::optional<int>(int)> g =
[](const int c) -> std::optional<int> {
if (c < 0) return std::nullopt;
else return c;
};
// another unary function that CAN fail.
std::function<std::optional<bool>(int)> f =
[](const int d) -> std::optional<bool> {
if (d < 10) return true;
else return std::nullopt;
};
// compose f, g, and h
auto G = Maybe(f) << Maybe(g) << Maybe(h);
// evaluate the composition - this maps (float, float) -> optional<bool>
auto result = G(1.0, 7.0);
// and check if the computation was successful
if (result) std::cout << "Result: " << *result << "\n";
else std::cout << "Computation failed!\n";
Here is my current implementation:
#include <functional>
#include <optional>
#include <iostream>
template <typename TReturn, typename... TArgs>
struct Maybe {
/**
* The (lifted) function that we evaluate.
*/
std::function<std::optional<TReturn>(const std::optional<TArgs>...)> eval_;
/**
* Lift a non-failable function into the Maybe monad.
*/
auto lift(std::function<TReturn(const TArgs...)> const& f) {
// construct a lambda that implements the Maybe monad.
return [f](const std::optional<TArgs> ... args) -> std::optional<TReturn> {
if ((args && ...)) return f(*(args)...);
else return {};
};
}
/**
* Lift a (failable) function returning an optional into the Maybe monad.
*/
auto lift(std::function<std::optional<TReturn>(const TArgs...)> const& f) {
// this overload is currently necessary so that I can extract the TReturn
// value type so that `eval_` doesn't pick up another layer of std::optional
// i.e. std::optional<std::optional<int(float, float)>>.
// construct a lambda that implements the Maybe monad.
return [f](const std::optional<TArgs> ... args) -> std::optional<TReturn> {
if ((args && ...)) return f(*(args)...);
else return {};
};
}
/**
* Construct a Maybe from a std::function returning an optional.
*/
Maybe(std::function<TReturn(TArgs...)> const f) : eval_(lift(f)) {}
/**
* Construct a Maybe from a std::function returning an optional.
*/
Maybe(std::function<std::optional<TReturn>(TArgs...)> const f) : eval_(lift(f)) {}
/**
* Apply the Maybe to the given arguments.
*/
auto operator()(std::optional<TArgs> const... args) const {
return this->eval_(args...);
}
/**
* Compose the callable in `this` with the callable in `other`.
*
* @param other Another monadic filter instance.
*/
template <typename TOReturn, typename... TOArgs>
auto operator<<(Maybe<TOReturn, TOArgs...> const& other) const -> Maybe<TReturn, TOArgs...> {
// get references to the underlying lifted functions
// capturing the Maybe instances into the lambda results in a seg-fault
auto f = this->eval_;
auto g = other.eval_;
// construct the coposition lambda
std::function<std::optional<TReturn>(TOArgs...)> fg =
[=](TOArgs... args) -> std::optional<TReturn> { return f(g(args...)); };
return fg;
}
}; // END: class Maybe
This is targeting C++17 only. Any and all feedback appreciated!
There's currently some duplication in the constructors and the lift method so that I wrap functions that already return std::optional without being wrapped in a second layer of optional i.e. std::optional<std::optional<...>> which makes the composition impossible (I'm sure there is some template trickery that could make this work with just a single method and constructor).