62 votes

Y a-t-il un nom pour cet idiome de création de tuple ?

Sur le Liste de diffusion Boost L'astuce suivante pour créer une entité de type tuple a été récemment publiée par @LouisDionne :

#include <iostream>

auto list = [](auto ...xs) { 
    return [=](auto access) { return access(xs...); }; 
}; 

auto length = [](auto xs) { 
    return xs([](auto ...z) { return sizeof...(z); }); 
};

int main()
{
    std::cout << length(list(1, '2', "3")); // 3    
}

Exemple en direct .

L'astuce est que list est un lambda prenant une liste de paramètres variadiques en entrée, et retournant un lambda en sortie qui prendra un autre lambda pour agir sur son entrée. De même, length est un lambda prenant une entité de type liste, à laquelle il fournira la variable sizeof... aux paramètres d'entrée originaux de la liste. Le site sizeof... est enveloppé dans un lambda afin qu'il puisse être passé à l'opérateur list .

Question Il existe un nom pour cet idiome de création de tuple. Peut-être d'un langage de programmation fonctionnel où les fonctions d'ordre supérieur sont plus couramment utilisées.

32voto

Manu343726 Points 8803

Je pense qu'il s'agit d'une mise en œuvre subtile d'une chose de type monade, plus précisément quelque chose dans le même esprit que la monade de continuation.

Les monades sont une construction de programmation fonctionnelle utilisée pour simuler l'état entre les différentes étapes d'un calcul (rappelons qu'un langage fonctionnel est sans état).
Ce que fait une monade, c'est enchaîner différentes fonctions, créant ainsi une "pipeline de calcul" où chaque étape connaît l'état actuel du calcul.

Les monades ont deux piliers principaux :

  • Une fonction de retour, qui prend une valeur et la renvoie sous une forme prête pour les Monad.
  • Une fonction de liaison, qui prend une valeur prête pour le Monad (de l'étape précédente du pipeline) et la déballe à son état d'origine pour transmettre la valeur à l'étape suivante.

La Wikipédia contient de très bons exemples et explications sur les monades.

Laissez-moi réécrire le code C++14 donné :

auto list = []( auto... xs ) 
{ 
    return [=]( auto access ) { return access(xs...); };
};

Je pense que nous identifions ici le return fonction d'une monade : Prend la valeur et la renvoie d'une manière monadique. Plus précisément, ce retour renvoie un foncteur (au sens mathématique, pas un foncteur C++) qui va de la catégorie "tuple" à la catégorie "variadic pack".

auto pack_size = [](auto... xs ) { return sizeof...(xs); };

pack_size est juste une fonction normale. Elle serait utilisée dans un pipeline pour effectuer un travail.

auto bind = []( auto xs , auto op ) 
{
    return xs(op);
};

Et length n'est qu'une version non-générique de quelque chose de proche de la monade bind opérateur, un opérateur qui prend une valeur monadique d'une étape précédente du pipeline, et la contourne vers la fonction spécifiée (la fonction qui fait réellement le travail). Cette fonction est la fonctionnalité effectuée par cette étape de calcul.

Finalement, votre appel pourrait être réécrit comme suit :

auto result = bind(list(1,'2',"3"), pack_size);

Donc, Quel est le nom de cet idiome de création de tuple ? Eh bien, je pense que cela pourrait être appelé " tuples de type monade ", puisque ce n'est pas exactement une monade, mais la représentation et l'expansion du tuple fonctionnent de manière similaire, restant à la monade de continuation de Haskell.

Edit : Plus de plaisir

Juste pour le plaisir de programmer en C++, j'ai continué à explorer ce genre de monade. Vous pouvez trouver quelques exemples aquí .

18voto

Sumant Points 2294

J'appellerais cet idiome tuple-continuateur ou plus généralement, monadique-continuateur . Il s'agit sans aucun doute d'une instance d'un monade de continuation. Une excellente introduction aux monades de continuation pour les programmeurs C++ est la suivante aquí . En substance, le list lambda ci-dessus prend une valeur (un paquet de paramètres variadiques) et renvoie un simple 'continuateur' (la fermeture interne). Ce continuateur, lorsqu'on lui donne un appelable (appelé access ), y passe le paquet de paramètres et renvoie ce que cet appelant renvoie.

En s'inspirant du blogue de FPComplete, un continuateur ressemble plus ou moins à ce qui suit.

template<class R, class A>
struct Continuator {
    virtual ~Continuator() {}
    virtual R andThen(function<R(A)> access) = 0;
};

El Continuator ci-dessus est abstrait - il ne fournit pas d'implémentation. En voici donc une simple.

template<class R, class A>
struct SimpleContinuator : Continuator<R, A> {
    SimpleContinuator (A x) : _x(x) {}
    R andThen(function<R(A)> access) {
        return access(_x);
    }
    A _x;
};

El SimpleContinuator accepte une valeur de type A et le transmet à access cuando andThen est appelé. Le site list lambda ci-dessus est essentiellement le même. Elle est plus générale. Au lieu d'une valeur unique, la fermeture interne capture un paquet de paramètres et le transmet à la fonction access fonction. Super !

J'espère que cela explique ce que signifie être un continuateur. Mais que signifie être une monade ? Voici un bon exemple introduction en utilisant des images.

Je pense que le list lambda est également un monade de liste, qui est implémenté comme un monade de continuation. Notez que la monade de continuation est la mère de toutes les monades . En d'autres termes, vous pouvez implémenter n'importe quelle monade avec une monade de continuation. Bien sûr, le monad de liste n'est pas hors de portée.

Comme un paquet de paramètres est tout naturellement une "liste" (souvent de types hétérogènes), il est logique qu'il fonctionne comme une monade liste/séquence. Le site list lambda ci-dessus est un moyen très intéressant de convertir les paquets de paramètres C++ en une structure monadique. Ainsi, les opérations peuvent être enchaînées les unes après les autres.

El length Le lambda ci-dessus, cependant, est un peu décevant car il brise la monade et le lambda imbriqué à l'intérieur renvoie simplement un entier. Il y a sans doute une meilleure façon d'écrire le "getter" de la longueur comme indiqué ci-dessous.

----Functeur----

Avant de pouvoir dire que le lambda list est une monade, nous devons montrer que c'est un foncteur. C'est-à-dire que fmap doit être écrit pour list.

La liste lambda ci-dessus sert de créateur du foncteur à partir d'un paquet de paramètres ----essentiellement, elle sert d'outil de création d'une liste. return . Le foncteur ainsi créé garde le paquet de paramètres avec lui (capture) et permet d'y "accéder" à condition de fournir un appelable qui accepte un nombre variable d'arguments. Notez que le callable est appelé EXACTEMENT UNE FOIS.

Écrivons fmap pour un tel foncteur.

auto fmap = [](auto func) { 
    return [=](auto ...z) { return list(func(z)...); };
};

Le type du func doit être (a -> b). C'est-à-dire, en langage C++,

template <class a, class b>
b func(a);

Le type de fmap est fmap: (a -> b) -> list[a] -> list[b] C'est-à-dire, en langage C++,

template <class a, class b, class Func>
list<b> fmap(Func, list<a>);

C'est-à-dire que fmap fait simplement correspondre une liste de-a à une liste de-b.

Maintenant, vous pouvez faire

auto twice = [](auto i) { return 2*i; };
auto print = [](auto i) { std::cout << i << " "; return i;};
list(1, 2, 3, 4)
    (fmap(twice))
    (fmap(print)); // prints 2 4 6 8 on clang (g++ in reverse)

Par conséquent, il s'agit d'un foncteur.

----Monad----

Maintenant, essayons d'écrire un flatmap (alias bind , selectmany )

Le type de carte plate est flatmap: (a -> list[b]) -> list[a] -> list[b].

C'est-à-dire qu'étant donné une fonction qui fait correspondre a à une liste-de-b et une liste-de-a, flatmap renvoie une liste-de-b. Essentiellement, elle prend chaque élément de la liste-de-a, appelle func dessus, reçoit la liste-de-b (potentiellement vide) un par un, puis concatène toutes les listes-de-b, et enfin renvoie la liste-de-b finale.

Voici une implémentation de flatmap pour la liste.

auto concat = [](auto l1, auto l2) {
    auto access1 = [=](auto... p) {
      auto access2 = [=](auto... q) {
        return list(p..., q...);
      };
      return l2(access2);
    };
    return l1(access1);
};

template <class Func>
auto flatten(Func)
{
  return list(); 
}

template <class Func, class A>
auto flatten(Func f, A a)
{
  return f(a); 
}

template <class Func, class A, class... B>
auto flatten(Func f, A a, B... b)
{
  return concat(f(a), flatten(f, b...));
}

auto flatmap = [](auto func) {
  return [func](auto... a) { return flatten(func, a...); };
};

Maintenant, vous pouvez faire beaucoup de choses puissantes avec une liste. Par exemple,

auto pair  = [](auto i) { return list(-i, i); };
auto count = [](auto... a) { return list(sizeof...(a)); };
list(10, 20, 30)
    (flatmap(pair))
    (count)
    (fmap(print)); // prints 6.

La fonction count est une opération de préservation de la monade car elle renvoie une liste à élément unique. Si vous voulez vraiment obtenir la longueur (non enveloppée dans une liste), vous devez terminer la chaîne monadique et obtenir la valeur comme suit.

auto len = [](auto ...z) { return sizeof...(z); }; 
std::cout << list(10, 20, 30)
                 (flatmap(pair))
                 (len);

S'il est bien fait, le pipeline de collecte (par exemple, filter , reduce ) peut désormais être appliqué aux packs de paramètres C++. Super !

----Les lois des monades----

Assurons-nous que le list La monade satisfait les trois lois des monades .

auto to_vector = [](auto... a) { return std::vector<int> { a... }; };

auto M = list(11);
std::cout << "Monad law (left identity)\n";
assert(M(flatmap(pair))(to_vector) == pair(11)(to_vector));

std::cout << "Monad law (right identity)\n";
assert(M(flatmap(list))(to_vector) == M(to_vector));

std::cout << "Monad law (associativity)\n";
assert(M(flatmap(pair))(flatmap(pair))(to_vector) == 
       M(flatmap([=](auto x) { return pair(x)(flatmap(pair)); }))(to_vector));

Toutes les assertions sont satisfaites.

----Collection Pipeline----

Bien que le lambda 'list' ci-dessus soit manifestement un monade et partage les caractéristiques du proverbial 'list-monad', il est assez désagréable. En particulier, parce que le comportement des pipeline de collecte combinateurs, tels que filter (alias where ) ne répond pas aux attentes communes.

La raison en est simplement le fonctionnement des lambdas C++. Chaque expression lambda produit un objet fonction d'un type unique. Par conséquent, list(1,2,3) produit un type qui n'a rien à voir avec list(1) et une liste vide, qui dans ce cas serait list() .

La mise en œuvre simple de where échoue à la compilation car en C++ une fonction ne peut pas retourner deux types différents.

auto where_broken = [](auto func) {
  return flatmap([func](auto i) { 
      return func(i)? list(i) : list(); // broken :-(
  }); 
};

Dans l'implémentation ci-dessus, func renvoie un booléen. C'est un prédicat qui dit vrai ou faux pour chaque élément. L'opérateur ? : ne compile pas.

On peut donc utiliser une autre astuce pour permettre la poursuite de la filière de collecte. Au lieu de filtrer réellement les éléments, ils sont simplement signalés comme tels - et c'est ce qui rend la situation désagréable.

auto where_unpleasant = [](auto func) {
  return [=](auto... i) { 
      return list(std::make_pair(func(i), i)...);
  }; 
};

El where_unpleasant fait le travail, mais de façon désagréable...

Par exemple, voici comment vous pouvez filtrer les éléments négatifs.

auto positive = [](auto i) { return i >= 0; };
auto pair_print = [](auto pair) { 
  if(pair.first) 
     std::cout << pair.second << " "; 
  return pair; 
};
list(10, 20)
    (flatmap(pair))
    (where_unpleasant(positive))
    (fmap(pair_print)); // prints 10 and 20 in some order

----Tuples hétérogènes----

Jusqu'à présent, la discussion a porté sur les tuples homogènes. Maintenant, généralisons-la aux vrais tuples. Cependant, fmap , flatmap , where ne prennent qu'un seul callback lambda. Pour fournir plusieurs lambdas travaillant chacun sur un type, nous pouvons les surcharger. Par exemple,

template <class A, class... B>
struct overload : overload<A>, overload<B...> {
  overload(A a, B... b) 
      : overload<A>(a), overload<B...>(b...) 
  {}  
  using overload<A>::operator ();
  using overload<B...>::operator ();
};

template <class A>
struct overload<A> : A{
  overload(A a) 
      : A(a) {} 
  using A::operator();
};

template <class... F>
auto make_overload(F... f) {
  return overload<F...>(f...);   
}

auto test = 
   make_overload([](int i) { std::cout << "int = " << i << std::endl; },
                 [](double d) { std::cout << "double = " << d << std::endl; });
test(10); // int 
test(9.99); // double    

Utilisons la technique du lambda surchargé pour traiter un continuateur de tuple hétérogène.

auto int_or_string = 
        make_overload([](int i) { return 5*i; },
                      [](std::string s) { return s+s; });
    list(10, "20")
        (fmap(int_or_string))
        (fmap(print)); // prints 2020 and 50 in some order

Enfin, Exemple en direct

3voto

Alexandre C. Points 31758

Cela ressemble à une forme de poursuite du style de passe .

L'idée générale du CPS est la suivante : au lieu d'avoir une fonction (disons f ) retournent une valeur, vous donnez à f un autre argument, qui est une fonction, appelé un continuation . Alors, f appelle cette continuation avec la valeur de retour au lieu de retourner. Prenons un exemple :

int f (int x) { return x + 42; }

devient

void f (int x, auto cont) { cont (x + 42); }

L'appel est un appel de queue, et peut être optimisé en un saut (c'est pourquoi le TCO est obligatoire dans certains langages, comme Scheme, dont la sémantique repose sur une certaine forme de transformation en CPS).

Un autre exemple :

void get_int (auto cont) { cont (10); }
void print_int (int x) { printf ("%d", x), }

Vous pouvez maintenant faire get_int (std::bind (f, _1, print_int)) pour imprimer 54. Notez que tous les appels de continuation sont toujours les appels de queue (l'appel à printf est également un appel de continuation).

Un exemple bien connu est celui des callbacks asynchrones (appels AJAX en javascript par exemple) : vous passez une continuation à une routine qui s'exécute en parallèle.

Les continuations peuvent être composées (et forment une monade au cas où cela vous intéresserait), comme dans l'exemple ci-dessus. En effet c'est possible pour transformer un programme (fonctionnel) entièrement en CPS, de sorte que chaque appel soit un appel de queue (et alors vous n'avez pas besoin de pile pour exécuter le programme !)

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