Search code examples
c++generic-programmingc++17c++-concepts

How do I constrain a lazy composition before I know the callable arguments?


So I'm playing around with GCC6 and its concepts implementation and I figured the Haskell Prelude would be a good source for experimenting. One of the core features of Haskell is function composition and this is something I needed to tackle straight away.

Mimicking the Haskell syntax as best that I could, I wrote this function:

template <typename F, typename G>
auto operator*(F f, G g)
{
  return [f, g](auto... args) {
    return f(g(args...));
  }
}

Which works great and allows me to do stuff like:

auto add([](int a, int b) { return a + b; }
auto doubled([](int a) { return a * 2; }

auto add_then_double(doubled * add);
assert(add_then_double(2, 3) == 10);

Happy, I decided to go back and apply some constraints to my function composition but I quickly hit a problem due to its laziness.

First I wrote this concept:

template <typename F, typename Ret, typename... Args>
concept bool Function()
{
  return requires(F f, Args ...args) {
    { f(args...) } -> Ret;
  }
}

Which I based off of the concepts found in Andrew Sutton's origin github project.

And so I tried to apply to my original function. The problem I have is that I don't know what G returns without knowing what arguments are passed to G so I can't constrain G and I don't know what F returns without knowing what parameter it is given and I don't know that because I don't know what G returns.

I'm pretty sure I need a new Function concept that doesn't care about the return type as my composition function doesn't care what F returns, so long as it's invokable. And I guess I could put the constraint on the inner lambda that the parameter types and correct for G and therefore for F but this means I can compose non-composable functions and won't get an error until the call site. Is this avoidable?

Maybe something like this:

template <typename F, typename G>
auto operator*(F f, G g)
{
  return [f, g](auto... args) 
    // is it even possible to constrain here?
    requires FunctionAnyReturn<G, decltype(args)...>
      && FunctionAnyReturn<F, decltype(G(decltype(args)...))>
  {
    return f(g(args...));
  }
}

Is this the best I can do (if I can even do that)?


Solution

  • As you discovered, it is indeed important to put constraints at the right place. In your case it is the operator() of the result that must be constrained, not the composition function itself. You really can't do any better, consider for instance that many functions don't have a single return type (e.g. std::make_tuple). However while Concepts-Lite does touch on lambda expressions a little, it doesn’t go as far as allowing a requires clause on them, so your attempt will not work.

    In most situations my usual advice is to write the lambda expression such that the resulting operator() is naturally constrained thanks to SFINAE. In your case, this means avoiding return type deduction:

    return [f, g](auto... args) -> decltype( f(g(args...)) )
    { return f(g(args...)); }
    

    If you’re using e.g. Clang, everything is peachy. If using GCC, you may run into a bug where GCC performs some checking too early.

    An alternative is to go out of your way to 'unroll' the closure type of the lambda expression. By making it a user-defined type, you gain access to all the tricks and in particular you can then write the explicit constraints you want:

    template<typename F, typename G>
    struct compose_type {
        F first_composed_function;
        G second_composed_function;
    
        template<typename... Args>
        constexpr auto operator()(Args... args)
            // substitute in whichever concepts and traits you're actually using
            requires
                Callable<G, Args...>
                && Callable<F, result_of<G, Args...>>
        { return first_composed_function(second_composed_function(args...)); }
    };
    
    template<typename F, typename G>
    constexpr compose_type<F, G> compose(F f, G g)
    { return { std::move(f), std::move(g) }; }
    

    Live On Coliru