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>> {
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.
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.