65 votes

Programmation fonctionnelle en C++. Mise en œuvre de f(a)(b)(c)

Je me suis lancé dans les bases de la programmation fonctionnelle avec C++. J'essaie de créer une fonction f(a)(b)(c) qui renverra a + b + c . J'ai implémenté avec succès la fonction f(a)(b) qui renvoie a + b. Voici le code correspondant :

std::function<double(double)> plus2(double a){
    return[a](double b){return a + b; };
}

Je n'arrive pas à comprendre comment implémenter la fonction f(a)(b)(c) qui, comme je l'ai dit précédemment, devrait retourner a + b + c .

38 votes

Puis-je vous demander pourquoi exactement vous essayez de faire de la programmation fonctionnelle en c++ (par opposition à tout autre langage intrinsèquement fonctionnel) ?

12 votes

Je ne sais pas si vous pouvez le faire avec juste f(a)(b)(c) . Vous devriez être capable de le faire fonctionner assez facilement si vous voulez utiliser f(a)(b)(c)() .

2 votes

@BenSteffan Je pratique le C++ ces derniers temps et je souhaite approfondir au maximum le langage et ses bibliothèques, et j'ai voulu essayer la bibliothèque fonctionnelle. J'ai implémenté la fonction f(a)(b) facilement, mais je n'arrive pas à comprendre comment implémenter f(a)(b)(c), et je ne veux pas aller plus loin avant d'avoir trouvé la solution xD

114voto

Jonas Points 5829

Vous pouvez le faire en ayant votre fonction f retourner un foncteur c'est-à-dire un objet qui implémente operator() . Voici une façon de le faire :

struct sum 
{
    double val;

    sum(double a) : val(a) {}

    sum operator()(double a) { return val + a; }

    operator double() const { return val; }
};

sum f(double a)
{
    return a;
}

Exemple

Lien

int main()
{
    std::cout << f(1)(2)(3)(4) << std::endl;
}

Version du modèle

Vous pouvez même écrire une version modélisée qui laissera le compilateur déduire le type. Essayez-le aquí .

template <class T>
struct sum 
{
    T val;

    sum(T a) : val(a) {}

    template <class T2>
    auto operator()(T2 a) -> sum<decltype(val + a)> { return val + a; }

    operator T() const { return val; }
};

template <class T>
sum<T> f(T a)
{
    return a;
}

Exemple

Dans cet exemple, T se résoudra finalement à double :

std::cout << f(1)(2.5)(3.1f)(4) << std::endl;

4 votes

L'intérêt des lambdas et std::function c'est que vous no besoin de créer une classe juste pour implémenter () . Vous pouvez créer des fonctions réelles et les faire circuler. L'utilisation d'objets fonctionnels n'est pas considérée comme une bonne pratique du C++. Je n'ai pas déclassé mais ce poserait des problèmes si vous essayiez de l'utiliser pour l'itération ou pour toute autre méthode STL qui attend un lambda

1 votes

@Jonas Je suis d'accord avec Panagiotis. Je voulais implémenter ma fonction avec des lambdas et std::function. Je n'ai pas non plus voté contre votre réponse même si ce n'était pas la réponse que je cherchais :/

1 votes

Je n'ai pas rétrogradé mais regarde wikipedia . "En code fonctionnel, la valeur de sortie d'une fonction ne dépend que des arguments qui sont passés à la fonction, ainsi appeler une fonction f deux fois avec la même valeur pour un argument x produira le même résultat f(x) à chaque fois ; ceci est en contraste avec les procédures dépendant de l'état local ou global".

59voto

Uriel Points 10724

Il suffit de prendre votre solution à deux éléments et de l'étendre, en l'entourant d'un autre lambda.

Puisque vous voulez retourner un lambda qui obtient un double et renvoie un double Tout ce que vous avez à faire est d'envelopper votre type de retour actuel avec une autre fonction, et d'ajouter un lambda imbriqué dans votre lambda actuel (un lambda qui renvoie un lambda) :

std::function<std::function<double(double)>(double)> plus3 (double a){
    return [a] (double b) {
        return [a, b] (double c) {
            return a + b + c;
        };
    };
}

  • Comme @Ðn noté, vous pouvez sauter l'étape std::function<std::function<double(double)>(double)> et s'entendre avec auto :

    auto plus3 (double a){
        return [a] (double b) {
            return [a, b] (double c) { return a + b + c; };
        };
    }
  • Vous pouvez étendre cette structure pour chaque nombre d'éléments, en utilisant des lambdas imbriquées plus profondes. Démonstration pour 4 éléments :

    auto plus4 (double a){
        return [a] (double b) {
            return [a, b] (double c) {
                return [a, b, c] (double d) {
                    return a + b + c + d;
                };
            };
        };
    }

11 votes

Sympa, ça répond aux attentes de l'OP, mais ça ne s'étend pas bien.

4 votes

C'est exactement comme ça que j'ai essayé de l'implémenter mais je n'ai pas inclus std::function<double(double)>(double). Merci, cela a rendu beaucoup de choses plus claires et maintenant je sais comment imbriquer encore plus profondément (même si je ne veux pas aller plus loin que cela).

3 votes

Nice, est-il possible de généraliser à, par exemple, moins et plus d'éléments ?

30voto

vsoftco Points 3170

Voici une approche légèrement différente, qui renvoie une référence à *this de operator() de sorte que vous n'avez pas de copies flottant autour. C'est une implémentation très simple d'un foncteur qui stocke l'état et se replie à gauche de manière récursive sur lui-même :

#include <iostream>

template<typename T>
class Sum
{
    T x_{};
public:
    Sum& operator()(T x)
    {
        x_ += x;
        return *this;
    }
    operator T() const
    {
        return x_;
    }
};

int main()
{
    Sum<int> s;
    std::cout << s(1)(2)(3);
}

Live on Coliru

4 votes

Cela fonctionne pour n'importe quel nombre de (summand) et est récursif, c'est-à-dire qu'il ne nécessite pas de définir une autre fonction/lambda pour chaque niveau.

0 votes

Pardonnez mon ignorance, mais comment le compilateur sait-il qu'il doit appeler votre opérateur de cast ? Je vois qu'il ne peut pas vraiment faire autre chose, puisqu'il n'y a pas d'opérateur d'insertion défini, mais je n'avais pas réalisé que quelque chose comme ça pouvait fonctionner sans être explicite avec le cast.

1 votes

@DaveBranton Les règles du langage stipulent que le compilateur est autorisé à essayer au maximum une conversion définie par l'utilisateur. Dans notre cas, nous avons un tel opérateur de conversion, donc en cout << s(1)(2)(3) le compilateur voit d'abord qu'il ne peut pas simplement l'afficher, puis il recherche des opérateurs de conversion définis par l'utilisateur, et il trouve Sum::operator T() const . Il applique ensuite la conversion à T puis affiche le T si elle le peut (dans notre cas, elle le peut parce qu'il s'agit d'une int ).

15voto

Barry Points 45207

Ce n'est pas f(a)(b)(c) mais plutôt curry(f)(a)(b)(c) . Nous emballons f de telle sorte que chaque argument supplémentaire renvoie soit un autre curry ou invoque effectivement la fonction avec empressement. C'est C++17, mais peut être implémenté en C++11 avec un tas de travail supplémentaire.

Notez qu'il s'agit d'une solution pour faire un curry d'une fonction - ce qui est l'impression que j'ai eue de la question - et non d'une solution pour replier une fonction binaire.

template <class F>
auto curry(F f) {
    return [f](auto... args) -> decltype(auto) {
        if constexpr(std::is_invocable<F&, decltype(args)...>{}) {
            return std::invoke(f, args...);
        }
        else {
            return curry([=](auto... new_args)
                    -> decltype(std::invoke(f, args..., new_args...))
                {
                    return std::invoke(f, args..., new_args...);
                });
        }
    };  
}

J'ai sauté les références de transmission pour plus de concision. Un exemple d'utilisation serait :

int add(int a, int b, int c) { return a+b+c; }

curry(add)(1,2,2);       // 5
curry(add)(1)(2)(2);     // also 5
curry(add)(1, 2)(2);     // still the 5th
curry(add)()()(1,2,2);   // FIVE

auto f = curry(add)(1,2);
f(2);                    // i plead the 5th

4 votes

C++17 rend tout cela si simple. L'écriture de ma fonction curry en C++14 était beaucoup plus pénible. Notez que pour une efficacité totale, vous pouvez avoir besoin d'un objet fonction qui déplace conditionnellement son tuple d'état en fonction du contexte d'invocation. Cela peut nécessiter de découpler tup et peut-être f de la liste de capture lambda afin qu'il puisse être transmis avec une valeur différente selon la façon dont les () s'appelle. Une telle solution dépasse toutefois le cadre de cette question.

0 votes

@Yakk J'espère pouvoir le faire encore mieux rangé ! Je dois trouver le temps de réviser ce papier...

0 votes

Je ne vois rien là-dedans qui permette un transfert parfait du "this" de la lambda elle-même, donc si elle est invoquée dans un contexte maybe-rvalue, vous pouvez parfaitement transférer des choses capturées by-value. Cela pourrait faire l'objet d'une autre proposition. (D'ailleurs, comment faire la distinction entre la fonction lambda *this et le contexte de la méthode englobante *this contexte ? Hurm.)

12voto

Justin Time Points 563

Le moyen le plus simple auquel je pense pour faire cela est de définir plus3() en termes de plus2() .

std::function<double(double)> plus2(double a){
    return[a](double b){return a + b; };
}

auto plus3(double a) {
    return [a](double b){ return plus2(a + b); };
}

Cela combine les deux premières listes d'arguments en une seule liste d'arguments, qui est utilisée pour appeler plus2() . Cela nous permet de réutiliser notre code préexistant avec un minimum de répétitions, et peut facilement être étendu à l'avenir ; plusN() a juste besoin de retourner un lambda qui appelle plusN-1() qui passera l'appel à la fonction précédente à son tour, jusqu'à ce qu'elle atteigne plus2() . Il peut être utilisé comme suit :

int main() {
    std::cout << plus2(1)(2)    << ' '
              << plus3(1)(2)(3) << '\n';
}
// Output: 3 6

Étant donné que nous ne faisons qu'appeler en bas de la ligne, nous pouvons facilement transformer ceci en un modèle de fonction, ce qui élimine le besoin de créer des versions pour des arguments supplémentaires.

template<int N>
auto plus(double a);

template<int N>
auto plus(double a) {
    return [a](double b){ return plus<N - 1>(a + b); };
}

template<>
auto plus<1>(double a) {
    return a;
}

int main() {
    std::cout << plus<2>(1)(2)          << ' '
              << plus<3>(1)(2)(3)       << ' '
              << plus<4>(1)(2)(3)(4)    << ' '
              << plus<5>(1)(2)(3)(4)(5) << '\n';
}
// Output: 3 6 10 15

Voir les deux en action aquí .

2 votes

C'est génial ! Il s'adapte parfaitement et l'implémentation est simple et agréable.

0 votes

@Jonas Merci. Lorsque vous essayez d'étendre un cadre existant comme celui-ci, le plus simple est probablement de trouver un moyen de définir la version étendue en fonction de la version actuelle, à moins que cela ne soit trop compliqué. Je ne pense pas non plus que le prototype soit strictement nécessaire pour la version modélisée, mais il peut accroître la clarté dans certains cas.

0 votes

Il a cependant le défaut d'être exécuté au moment de l'exécution, mais cela peut être atténué dès que d'autres compilateurs prendront correctement en charge le programme constexpr lambdas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X