在知道可调用参数之前如何约束惰性组合?
How do I constrain a lazy composition before I know the callable arguments?
所以我正在研究 GCC6 及其概念实现,我认为 Haskell Prelude 将是一个很好的实验来源。 Haskell 的核心功能之一是函数组合,这是我需要立即解决的问题。
尽我所能模仿 Haskell 语法,我编写了这个函数:
template <typename F, typename G>
auto operator*(F f, G g)
{
return [f, g](auto... args) {
return f(g(args...));
}
}
效果很好,可以让我做类似的事情:
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);
很高兴,我决定回去对我的函数组合应用一些约束,但由于它的惰性,我很快遇到了问题。
首先我写了这个概念:
template <typename F, typename Ret, typename... Args>
concept bool Function()
{
return requires(F f, Args ...args) {
{ f(args...) } -> Ret;
}
}
我基于 Andrew Sutton's origin github 项目中的概念。
所以我尝试将其应用于我的原始功能。我遇到的问题是,在不知道传递给 G
的参数的情况下,我不知道 G
return 是什么,所以我无法约束 G
而且我不知道在不知道给出什么参数的情况下不知道 F
returns 是什么,我不知道是因为我不知道 G
returns.
我很确定我需要一个新的 Function
概念,它不关心 return 类型,因为我的组合函数不关心什么 F
returns,只要它是可调用的。而且我想我可以对参数类型的内部 lambda 施加约束,并针对 G 和 F 进行更正,但这意味着我可以编写不可组合的函数,并且在调用站点之前不会出错。这是可以避免的吗?
也许是这样的:
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...));
}
}
这是我能做到的最好的(如果我能做到的话)吗?
如您所见,将约束放在正确的位置确实很重要。在您的情况下,必须约束结果的 operator()
,而不是组合函数本身。你真的不能做得更好,例如考虑许多函数没有单一的 return 类型(例如 std::make_tuple
)。然而,虽然 Concepts-Lite 确实稍微涉及到 lambda 表达式,但它并没有允许在它们上面使用 requires
子句,因此您的尝试不会奏效。
在大多数情况下,我通常的建议是编写 lambda 表达式,以便生成的 operator()
自然受到 SFINAE 的约束。在您的情况下,这意味着避免 return 类型推导:
return [f, g](auto... args) -> decltype( f(g(args...)) )
{ return f(g(args...)); }
如果您使用的是铿锵,everything is peachy. If using GCC, you may run into a bug where GCC performs some checking too early.
另一种方法是特意选择 'unroll' lambda 表达式的闭包类型。通过使它成为用户定义的类型,您可以获得所有技巧,特别是您可以编写您想要的显式约束:
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) }; }
所以我正在研究 GCC6 及其概念实现,我认为 Haskell Prelude 将是一个很好的实验来源。 Haskell 的核心功能之一是函数组合,这是我需要立即解决的问题。
尽我所能模仿 Haskell 语法,我编写了这个函数:
template <typename F, typename G>
auto operator*(F f, G g)
{
return [f, g](auto... args) {
return f(g(args...));
}
}
效果很好,可以让我做类似的事情:
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);
很高兴,我决定回去对我的函数组合应用一些约束,但由于它的惰性,我很快遇到了问题。
首先我写了这个概念:
template <typename F, typename Ret, typename... Args>
concept bool Function()
{
return requires(F f, Args ...args) {
{ f(args...) } -> Ret;
}
}
我基于 Andrew Sutton's origin github 项目中的概念。
所以我尝试将其应用于我的原始功能。我遇到的问题是,在不知道传递给 G
的参数的情况下,我不知道 G
return 是什么,所以我无法约束 G
而且我不知道在不知道给出什么参数的情况下不知道 F
returns 是什么,我不知道是因为我不知道 G
returns.
我很确定我需要一个新的 Function
概念,它不关心 return 类型,因为我的组合函数不关心什么 F
returns,只要它是可调用的。而且我想我可以对参数类型的内部 lambda 施加约束,并针对 G 和 F 进行更正,但这意味着我可以编写不可组合的函数,并且在调用站点之前不会出错。这是可以避免的吗?
也许是这样的:
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...));
}
}
这是我能做到的最好的(如果我能做到的话)吗?
如您所见,将约束放在正确的位置确实很重要。在您的情况下,必须约束结果的 operator()
,而不是组合函数本身。你真的不能做得更好,例如考虑许多函数没有单一的 return 类型(例如 std::make_tuple
)。然而,虽然 Concepts-Lite 确实稍微涉及到 lambda 表达式,但它并没有允许在它们上面使用 requires
子句,因此您的尝试不会奏效。
在大多数情况下,我通常的建议是编写 lambda 表达式,以便生成的 operator()
自然受到 SFINAE 的约束。在您的情况下,这意味着避免 return 类型推导:
return [f, g](auto... args) -> decltype( f(g(args...)) )
{ return f(g(args...)); }
如果您使用的是铿锵,everything is peachy. If using GCC, you may run into a bug where GCC performs some checking too early.
另一种方法是特意选择 'unroll' lambda 表达式的闭包类型。通过使它成为用户定义的类型,您可以获得所有技巧,特别是您可以编写您想要的显式约束:
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) }; }