Similar to Partial Function composability in Haskell, I've attempted to implement partial function composability in C++20 via C++ concepts. More details about the problem(for which my solution is available below) from the linked post:
Below is my solution for the CTFP chapter 4 challenges which essentially involves composing partial functions(that don't have defined outputs for all possible inputs i.e. returning a Maybe/std::optional in C++). The challenge is to implement composition, identity(to satisfy category requirements) and try it out with 2 partial functions.
#include <optional>
#include <cmath>
#include <cassert>
#include <concepts>
// C++17 alternative with std::function: http://coliru.stacked-crooked.com/a/d8a8ed976dc59715
namespace detail{
template<typename>
struct unary_fn;
template<typename M, typename R, typename Arg>
struct unary_fn<R(M::*)(Arg)>
{
using return_type = R;
using arg_type = Arg;
};
template<typename R, typename Arg>
struct unary_fn<R(*)(Arg)>
{
using return_type = R;
using arg_type = Arg;
};
template<typename Lambda>
struct unary_fn : unary_fn<decltype(&Lambda::operator())> { };
template <typename T>
struct is_optional: std::false_type {};
template <typename T>
struct is_optional<std::optional<T>>: std::true_type {};
}
// Concept to check composability
template <typename PartialFn1, typename PartialFn2, typename OutType = detail::unary_fn<PartialFn2>::return_type, typename InType = detail::unary_fn<PartialFn1>::arg_type>
concept SingleArgComposable = requires(PartialFn1 pf1, PartialFn2 pf2, InType elem) {
requires detail::is_optional<typename detail::unary_fn<PartialFn1>::return_type>::value;
{ pf2(*pf1(elem)) } -> std::same_as<OutType>;
requires detail::is_optional<OutType>::value;
};
// Given(using std::optional) and safe_root
std::optional<double> safe_root(double x) {
return x >= 0 ? std::optional{std::sqrt(x)} : std::nullopt;
}
// Q1: Construct the Kleisli category for partial functions (define composition and identity).
template <typename T>
std::optional<T> id(T input) {
return input;
}
template <typename PartialFn1, typename PartialFn2, typename RetType = detail::unary_fn<PartialFn2>::return_type> requires SingleArgComposable<PartialFn1, PartialFn2, RetType>
auto compose(PartialFn1 firstCallable, PartialFn2 secondCallable) {
return [firstCallable, secondCallable](auto inValue) {
auto firstResult = firstCallable(inValue);
if(firstResult) {
return secondCallable(*firstResult);
}
return RetType{};
};
}
// Q2: Implement safe_reciprocal
std::optional<double> safe_reciprocal(double input) {
return (input != 0) ? std::optional{1 / input} : std::nullopt;
}
int main(){
static_assert(detail::is_optional<std::optional<double>>::value);
// Q1(Testing id + compose)
assert((safe_root(4.) == compose(id<double>, safe_root)(4.)) && safe_root(4.) == std::optional<double>{2.});
assert((safe_root(4.) == compose(safe_root, id<double>)(4.)) && safe_root(4.) == std::optional<double>{2.});
assert((safe_root(-5.) == compose(safe_root, id<double>)(-5.)) && safe_root(-5.) == std::nullopt);
// Q3: Compose safe_root and safe_reciprocal
auto safe_root_reciprocal = compose(safe_reciprocal, safe_root);
assert(*safe_root_reciprocal(0.25) == 2.0);
assert(safe_root_reciprocal(0) == std::nullopt);
assert(safe_root_reciprocal(-0.25) == std::nullopt);
}
Hoping to get suggestions on:
- C++20 alternatives I can use to avoid having to implement "unary_fn".
- Any improvements I could make to the
SingleArgComposableconcept(to make it closer to the Haskell "concept"(b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)).std::functionis actually able to come pretty close(coliru link), but I was hoping to do this with concepts. - General review for improvements and C++20 features I should be using.
std::functionnot support all this? \$\endgroup\$std::functiondoes support this - I implemented it that way in the C ++17 link above: coliru.stacked-crooked.com/a/d8a8ed976dc59715. I was trying to implement this in a generic way to use regular/member/lambda and std::function using C++20 concepts. \$\endgroup\$