Шаблонное выражение - PullRequest
1 голос
/ 05 марта 2020

Мне нужен концепт Functor в C ++ 20.

Функтор - это тип с более высоким родом, который можно отображать. Простой пример: std::optional; с помощью функции от типа A до типа B и std::optional<A> вы можете легко создать std::optional<B>, применяя функцию к значению, если оно существует, и возвращая пустой optional в противном случае. Эта операция называется fmap в Haskell.

template<typename A, typename B>
std::optional<B> fmap(std::function<B(A)> f, std::optional<A> fa) {
    if (!fa) {
        return std::optional<B>{};
    }
    return std::optional<B>(f(*fa));
}

Концепция всех функторов достаточно проста для написания. Я придумал это (используя GCC - вам придется удалить bool, чтобы заставить это работать в Clang, я думаю):

template<template<typename> typename F, typename A, typename B>
concept bool Functor = requires(std::function<B(A)> f, F<A> fa) {
    { fmap(f, fa) } -> F<B>;
};

И простая дополнительная функция, чтобы убедиться, что это работает :

template<typename A, typename B>
std::function<B(A)> constant(B b) {
    return [b](A _) { return b; };
}

template<template<typename> typename F, typename A, typename B>
F<B> replace(B b, F<A> fa) requires Functor<F,A,B> {
    return fmap(constant<A,B>(b), fa);
}

Работает. Но это не красиво. То, что я хочу, чтобы подпись replace читалась так:

template<Functor F, typename A, typename B>
F<B> replace(B b, F<A> fa);

Нет необходимости в требовании здесь. Намного лучше, ты не согласен? Однако, чтобы это сработало, мне пришлось бы свести шаблон моей концепции к одному аргументу. Примерно так:

template<template<typename> typename F>
concept bool Functor = requires(function<B(A)> f, F<A> fa) {    // Uh-oh
    { fmap(f, fa) } -> F<B>;
};

Проблема в том, что я не объявил типы A и B. Насколько я могу судить, я нигде не могу объявить их, прежде чем использовать их. Можно ли сделать то, что я хочу, и можно ли сделать это просто и элегантно?

Одно из возможных решений, которое приходит мне в голову, - это сделать выражение потребности в концепции шаблоном (или, по крайней мере, подобным шаблону). предмет). Тогда у меня будет что-то вроде этого:

template<template<typename> typename F>
concept bool Functor = requires<typename A, typename B>(function<B(A)> f, F<A> fa) {
    { fmap(f, fa) } -> F<B>;
};

К сожалению, это не соответствует стандарту C ++ 20 и не будет компилироваться с g++-8. Может ли что-то подобное быть жизнеспособным? Может ли он сделать это в стандарте?

Ответы [ 2 ]

3 голосов
/ 05 марта 2020

C ++ не имеет параметров c полиморфизма, подобного этому - вы не можете делать такие вещи, как «для любого типа», так, как вы хотите, и так, как вы можете в Haskell. Я думаю, что это принципиально невозможно в мире, где существует перегрузка.

У вас есть следующее (я пошел дальше и удалил ошибочное bool, которое не является частью концепций C ++ 20, и исправил -> Type, который также был удален):

template<template<typename> class F, typename A, typename B>
concept Functor = requires(std::function<B(A)> f, F<A> fa) {
    { fmap(f, fa) } -> std::same_as<F<B>>;
};

То, что вы хотите сказать, относится к любым типам a и b, учитывая a -> b, вы можете вызвать эту функцию. Мы не можем этого сделать. Но мы можем сами выбирать произвольные типы. Один из способов сделать это - выбрать секретные типы, о которых реализация функтора просто не знает:

namespace secret {
    struct A { };
    struct B { };

    template <typename From, typename To>
    struct F {
        auto operator()(From) const -> To;
    };
}

template <template <typename> class F>
concept Functor = requires(secret::F<secret::A, secret::B> f, F<secret::A> fa) {
    { fmap(f, fa) } -> std::same_as<F<secret::B>>;
};

Возможно, это ваш лучший выбор. Вы можете даже добавить несколько пар a / b, чтобы сделать это с большей вероятностью правильным.

Независимо от этого:

template<Functor F, typename A, typename B>
F<B> replace(B b, F<A> fa);

В любом случае этого не произойдет, поскольку у нас нет такого краткого синтаксиса для ограниченных параметров шаблона шаблона. Вы должны написать это так:

template <template <typename> class F, typename A, typename B>
    requires Functor<F>
F<B> replace(B b, F<A> fa);

В качестве примечания, это плохая реализация fmap для optional:

template<typename A, typename B>
std::optional<B> fmap(std::function<B(A)> f, std::optional<A> fa);

Взятие std::function<Sig> означает, что это будет только работать, если вы передадите в конкретно a std::function. Не для лямбд, указателей функций или других объектов функций (например, secret::F, который я использовал ранее). И даже если бы это сработало, вы бы не захотели делать это в любом случае, поскольку это ненужные накладные расходы.

Вы хотите:

template <typename F, typename A, typename B = std::invoke_result_t<F&, A const&>>
std::optional<B> fmap(F f, std::optional<A> fa);

У меня есть целый пост об этой конкретной проблеме, Объявления, использующие понятия .

3 голосов
/ 05 марта 2020

Ваша концепция "Functor" представляет собой сложную взаимосвязь между тремя различными типами: F (шаблон с одним параметром, который является проецируемым шаблоном), A (тип начального объекта) и B (результирующий тип объекта). Ваша концепция представляет собой связь между 3 параметрами, поэтому ваша концепция должна будет принимать 3 параметра.

Краткий синтаксис шаблона предназначен для простых случаев: ограничения, относящиеся к одному (типу) параметру. Ваш случай не прост, поэтому вам придется изложить его с помощью предложения requires. Каждый раз, когда у вас есть концепция с несколькими параметрами, подобными этим, вам придется изложить ее.

Что касается того, является ли она «красивой» или нет, это ценностное суждение. Но учитывая сложную взаимосвязь, показанную здесь, ясно видно, какова взаимосвязь между всеми этими параметрами. И у ясности есть своя красота.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...