Search code examples
c++templatesfunctional-programmingc++17std-function

C++17: Want to write `hom<(A, B), C>` to alias `std::function<C(A, B)>`


This question comes from an attempt to make some code read like some mathematical notation. So I'm not trying to make things readable to a general software engineer. I'm also aware that this is more trouble than it's worth, but at this point, it's personal.

Let hom<X, Y> be the set of (referentially transparent) functions from a type X to type Y. I am not going for performance, so I'm happy using std::function under the covers.

I want to be able to write hom<A, B> as an alias for std::function<B(A)> AND hom<(A, B), C> as an alias for std::function<C(A, B)>. For single variable functions, of course the simple template alias,

template<typename Dom, typename Cod>
using hom = std::function<Cod(Dom)>;

works splendidly. The bivariate case, I realise, may be impossible. But I'll put it forth to see what the SE hive-mind can come up with. I DON'T need a general solution to multivariate functions: uni- and bi-variate cases suffice.

The goal is to be able to write structures such as

template <typename I, typename S, typename O>
struct MooreMachine {
  S s0;
  hom<(S, I), S> tmap;
  hom<S, O> rmap;
};

and function signatures such as

template <typename I, typename S>
auto rx_scanl(S s0, hom<(S, I), S> f)
    -> hom<rx::observable<I>, rx::observable<S>> {

Attempts

My attempts with preprocessor macros, of course, run into the parsing issues with the comma and this is a case where I can't put in extra parens.

After a lot of frustration, I relaxed my requirements to allow hom<A,B,C> to stand in for std::function<C(A,B)>.

I don't like that (because it pleases nobody, it doesn't fit the math and at least std::function is familiar, even if it is ugly). But I was surprised to find difficulty even with the relaxed notation! I naively tried overloading the template alias to find out that this isn't allowed. I also couldn't make progress with variadic templates since the parameter packs must be the final template argument for alias templates.

My best attempt was using a helper struct:

template<typename...>
struct hom_impl;

template<typename Dom, typename Cod>
struct hom_impl<Dom, Cod> {
    using type = std::function<Codomain(Dom)>;
};

template<typename Dom1, typename Dom2, typename Cod>
struct hom_impl<Dom1, Dom2, Cod> {
    using type = std::function<Cod(Dom1, Dom2)>;
};

template<typename... Doms>
using hom = typename hom_impl<Doms...>::type;

I have no idea why, but this interferes with overload resolution. Specifically, in something like make_cata below, it says it can't infer the template argument S:

template <typename S>
using OPI = std::optional<std::pair<S, Input>>;

template <typename S>
using OPIAlgebra = hom<OPI<S>, S>;

template <typename S>
auto make_cata(OPIAlgebra<S> alg) -> hom<std::vector<Input>, S> {
  // ...
}

Any ideas would be great. As I said, the problem is personal at this point. It feels like something I should be able to do, and I'm just being stubborn.

Like I said, I really would like hom<(A, B), C> or something close (like hom((A, B), C) in the macro case). Though my best attempt was hom<A, B, C>, this isn't quite the math notation and is strange to the programmer, so it really helps nobody.


Solution

  • For completeness, I'm posting @IgorTandetnik's solution. (If he would like to post his solution, I'll happily delete this and give the green check to him.) All credit goes to him.

      template <typename... Ts>
      struct Doms {};
    
      template <typename Dom, typename Cod>
      struct Hom : public std::function<Cod(Dom)> {
        using std::function<Cod(Dom)>::function;
      };
    
      template <typename Cod, typename... Ts>
      struct Hom<Doms<Ts...>, Cod>
          : public std::function<Cod(Ts...)> {
        using std::function<Cod(Ts...)>::function;
      };
    

    So Hom<A, Z> == std::function<Z(A)> and Hom<Doms<A₁, A₂, …>, Z> == std::function<Z(A₁, A₂, …)>

    It eschews standard advice to avoid inheriting from STL containers, but since there are no virtual destructors involved, I don't see any looming pathology here.