C++ Expressif

Problème avec les tuples

Bienvenue à la dernière d'une série d'articles sur langages orientés domaines enfoui (EDSL : Embedded Domain-Specific Languages) et Boost.Proto, une bibliothèque pour les implémenter en C ++. Préparez-vous ; ce dernier article touchera un peu de tout, partant du pattern matching du modèle Haskell aux tuples TR1, et profiter de la technique de composition de fonctions du dernier article -pour améliorer les 30 lignes de la bibliothèque de lambda que nous avions implémentées- qui, lorsque la poussière sera retombée, dépassera les 30 lignes. À la fin, nous saurons comment composer des calculs non triviaux sur les arbres d'expression à partir de blocs de construction simple, réclamer plus de territoire de programmation fonctionnelle pour notre propre, et même posséder une bibliothèque de lambdas un peu plus utile pour démontrer nos efforts.

Cet article a plus que ce que j'aurais aimé qu'il possède. Préparez une taxe de café et mettez-vous à l'aise. Cela peut prendre un bon moment.

Article lu   fois.

Les deux auteur et traducteur

Traducteur :

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. La Bibliothèque lambda minuscule

Dans les articles précédents, nous avons développé une petite bibliothèque de lambdas : une bibliothèque pour créer des fonctions anonymes sur place. (Les articles originaux sont ici et ici .) Depuis que je ferai référence revenir, voici la solution à ce jour:

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
#include <algorithm>
#include <boost/proto/proto.hpp>
using namespace boost;
 
// A type recognized by the Lambda algorithm below
// as a placeholder for a user-supplied argument.
struct arg1_tag {};
typedef proto::terminal<arg1_tag> arg1_term;
 
// Forward-declare our custom expression wrapper.
template<typename ProtoExpr> struct lambda_expr;
 
// Define a lambda domain and its generator, which
// wraps all new expressions our custom wrapper.
struct lambda_domain :
  proto::domain< proto::pod_generator<lambda_expr> >
{};
 
// A Proto algorithm for evaluating lambda
// expressions.
struct Lambda :
  proto::or_<
    // When evaluating the arg1 terminal,
    // return the state.
    proto::when<arg1_term, proto::_state>,
    // Otherwise, do the "default" thing.
    proto::otherwise< proto::_default< Lambda > >
  >
{};
// A lambda is an expression with an operator()
// that evaluates the lambda.
template<typename ProtoExpr>
struct lambda_expr
{
  BOOST_PROTO_BASIC_EXTENDS(
    ProtoExpr, lambda_expr, lambda_domain)
  BOOST_PROTO_EXTENDS_ASSIGN()
  BOOST_PROTO_EXTENDS_SUBSCRIPT()
 
  // So that boost::result_of can calculate
  // the return type of this lambda expression.
  template<typename Sig> struct result;
 
  template<typename T, typename A1>
  struct result<T(A1)> :
    boost::result_of<Lambda(T const&, A1 const&)>
  {};
 
  // Evaluate the lambda expressions
  template<typename A1>
  typename result<lambda_expr(A1)>::type
  operator()(A1 const & a1) const
  {
    return Lambda()(*this, a1);
  }
};
 
lambda_expr<arg1_term::type> const arg1 = {};

Le code ci-dessus définit un objet arg1 de telle sorte que les expressions qui impliquent créeent des fonctions lambda. Par exemple, nous pouvons à présent faire ceci :

 
Sélectionnez
int main()
{
    int data[] = {0,1,2,3,4,5,6,7,8,9};
 
    // Compute the squares
    std::transform(data, data+10, data, arg1 * arg1);
 
    // Write the result to std::cout
    std::for_each(data, data+10, std::cout << arg1 << ' ');
}

Le code ci-dessus affiche les éléments suivants:

 
Sélectionnez
0 1 4 9 16 25 36 49 64 81

Les expressions arg1 * arg1 et std::cout << arg1 << ' ' définissent les « fonctions » qui acceptent un argument et font quelque chose pour l'argument ; l'argument passé est remplacé par arg1 partout dans l'expression qui à son tour est évaluée selon les règles normales des expressions C++. Je dis « fonctions » parce que nous savons tous qu'ils ne sont pas vraiment des fonctions, ce sont des foncteurs -objects d'un type possédant une fonction membre operator(). Dans notre code, cette fonction membre est définie à la ligne 52.

L' operator() à la ligne 52 accepte un seul argument. Toutes les fonctions lambda créées de cette façon acceptent seulement un argument. C'est nul! Les fonctions C++ ordinaires n'ont pas cette limitation, et les lambdas devraient être comme les fonctions ordinaires C++ autant que possible. Nous en avons besoin...

Standard ML et Proto

Les Fonctions en Standard ML n'acceptent qu'une seule valeur(1). Pour passer plus d'arguments, vous devez les mettre dans un tuple. Comme nous le verrons, Proto a une restriction similaire et la solution est la même.

II. Plus d'arguments lambda !

Si nous voulons gérer plus d'un argument, certains changements sont nécessaires.

  1. Nous avons besoin de plus de surcharges de lambda_expr::operator() qui prennent plusieurs arguments.
  2. Nous avons besoin de plus d'espaces réservés en sus de arg1 pour stocker ces arguments supplémentaires.
  3. Nous avons également sans doute besoin de changer notre algorithme Lambda pour lier les arguments supplémentaires aux espaces supplémentaires réservés, mais la manière dont il faut le faire n'est pas encore définie.

Astuce : nous utiliserons la composition de fonctions du précédent tutoriel. (Oooh, présage.)

C'est ma liste de tâches à faire pour le reste du tutoriel. (1) et (2) devrait être assez facile ; (3) est délicat. Commençons depuis le haut avec (1).

II-A. Plus surcharges pour lambda_expr::operator()

Tuples

Les tuples sont une généralisation des std::pair avec un nombre variable d'éléments. Ils viennent de la norme en C++0x et TR1. Ils existent également en Boost avec une interface similaire (mais pas identique). J'utiliserai la bibliothèque de Tuple de Boost parce que cela fonctionne partout, en étant sûr de montrer en quoi il diffère de la norme.

Le premier ordre du jour est de modifier lambda_expr::operator() afin qu'elle accepte un nombre variable d'arguments. L'ancien lambda_expr::operator() accepte un seul argument et est directement passé à l'algorithme Lambda comme un argument d'état. Nous allons maintenant avoir beaucoup d'arguments mais un seul argument d'état. Alors plaçons les tous dans un tuple et transmettons les plutôt. Le code ci-dessous montre comment:

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
template<typename ProtoExpr>
struct lambda_expr
{
    // ...
 
    // one-argument operator() for evaluating unary lambdas
    template<typename A1>
    typename boost::result_of<Lambda(lambda_expr const &, boost::tuple<A1 const &> const &)>::type
    operator()(A1 const & a1) const
    {
        boost::tuple<A1 const &> const state(a1);
        return Lambda()(*this, state);
    }
 
    // two-argument operator() for evaluating binary lambdas
    template<typename A1, typename A2>
    typename boost::result_of<Lambda(lambda_expr const &, boost::tuple<A1 const &, A2 const &> const &)>::type
    operator()(A1 const & a1, A2 const & a2) const
    {
        boost::tuple<A1 const &, A2 const &> const state(a1, a2);
        return Lambda()(*this, state);
    }
 
    // ... more overloads for additional arguments
};

Déjà, nous pouvons voir quel gâchis cela va devenir. Pour faire face à N arguments, nous avons besoin de N surcharges de operator(), et cela est répétitif dans la plupart du temps. Le code ci-dessus est beaucoup plus agréable en C++0x.

Voici la version C++0x du jeu de surcharge ci-dessus en utilisant un modèle de fonction variadique pour réduire les répétitions de code, les références rvalue appropriées à la manipulation d'objets temporaires, et unifié la syntaxe d'initialisation d'« état » juste pour le plaisir.

 
CacherSélectionnez
template<typename... A>
typename std::result_of<Lambda(lambda_expr const &, std::tuple<A &&...> const &)>::type
operator()(A &&... a) const
{
  std::tuple<A &&...> const state{std::forward<A>(a)...};
  return Lambda()(*this, state);
}

Fournir par Hackadelic Sliding Notes 1.6.5

Les lignes 11 et 20 ci-dessus initialisent un tuple avec les arguments lambda et les transmettent à l'algorithme Lambda. Les types de retour noueux semble pire que ce qu'ils sont vraiment. Ils lisent: « Quel type d'objet l'algorithme Lambda retourne si je transmets un objet de type lambda_expr et tuple convenablement initialisé ? » Nous nous assurons de mettre const et & au bon endroit pour faire const-correct et éviter la copie inutile.

Je ne vais pas le montrer ici, mais nous avons également besoin d'ajouter N spécialisations partielles du modèle lambda_expr::result de sorte que les expressions lambda soient des foncteurs TR1 valides. (Vous ne pouvez donc pas attendre pour les modèles variadiques, ai-je raison ?) Vérifiez la solution finale pour les détails.

Le point (1) sur notre liste de tâches à faire est fait. Passons au point (2).

II-B. Plus d'espaces réservés

Arg1 est solitaire. Donnons-lui des amis arg2 et arg3 . Je vais le faire d'une façon curieuse qui aura de sens plus tard, au moment où nous serons sur le point (3).

Tout d'abord, nous allons faire disparaître arg1_tag et arg1_term et les remplacer par quelque chose de plus général : templates de classe pour les espaces réservés qui sont paramétré sur un indice de l'argument :

 
Sélectionnez
// A (mostly) empty type that can be used to define lambda
// placeholders. The template parameter is a compile-time
// integer that represents the placeholder number.
template< typename Index >
struct argN_tag
{
    // requires that Index models IntegralConstant
    typedef Index index;
};
 
// A helper for creating placeholder terminals
template< typename Index >
struct argN_term
  : proto::terminal< argN_tag< Index > >
{};

Le paramètre Index du template est un entier qui a été transformé en un type. Integral type wrappers viennent de la norme en C++0x et TR1 et portent le nom difficile à manier std::integral_constant. La bibliothèque Boost MPL en a de trop, de telle sorte qu'ils ont des noms comme mpl::int_, mpl::long_, etc. Je pourrais utiliser n'importe lequel ; ils signifient tous la même chose. Comme d'habitude, je vais m'en tenir à ce qui est dans Boost.

Voici comment nous définissons les espaces réservés aux arguments :

 
Sélectionnez
typedef mpl::int_<0> nil_t; // or std::integral_constant<int, 0>
typedef mpl::int_<1> one_t; // etc.
typedef mpl::int_<2> two_t;
 
// Define the argument placeholders:
lambda_expr< argN_term< nil_t >::type > const arg1 = {};
lambda_expr< argN_term< one_t >::type > const arg2 = {};
lambda_expr< argN_term< two_t >::type > const arg3 = {};

Bien ! Nous avons vérifié les points (1) et (2) de notre liste de tâches à exécuter. Nous pouvons créer des expressions lambda ternaires comme arg1 + arg2 + arg3, et l'objet résultant actuellement possède un membre operator() qui accepte trois arguments. Mais si nous essayons effectivement d'utiliser ce membre, nous trouverons des tas de problèmes ; nous n'avons pas encore lié les espaces réservés aux éléments du tuple. En avant.

II-C. Relier les espaces réservés aux éléments du tuple

L'algorihme Lambda est le centre névralgique de notre bibliothèque lambda. Il évalue les expressions lambda, en substituant les arguments réels aux espaces réservés. Maintenant que nous transmettons un tuple d'arguments à Lambda, il a besoin d'être modifié pour en compte ce fait. Revenons à l'implémentation de Lambda. La substitution des arguments réels aux espaces réservés est assurée par la ligne 25 :

Actuel code Proto
Sélectionnez
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
// A Proto algorithm for evaluating lambda
// expressions.
struct Lambda
  : proto::or_<
        // When evaluating the arg1 terminal,
        // return the state.
        proto::when< arg1_term, proto::_state >
        // Otherwise, do the "default" thing.
      , proto::otherwise< proto::_default< Lambda > >
    >
{};
Equivalent pseudo-code
Sélectionnez
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
// PSEUDO-CODE equivalent for the Lambda
// algorithm to the left.
auto Lambda( auto expr, auto state )
{
    // When evaluating the arg1 terminal,
    // return the state.
    if ( expr matches? arg1 ) return state;
    // Otherwise, do the "default" thing.
    if ( expr matches? ( _ + _ ) )
        return Lambda( child<0>(expr), state )
             + Lambda( child<1>(expr), state );
    if ( expr matches? ( _ - _ ) )
        return Lambda( child<0>(expr), state )
             - Lambda( child<1>(expr), state );
    // ...
}

Si le pseudo-code ci-dessus n'a pas aidé et que Lambda demeure un mystère, consultez une bibliothèque de lambdas en 30 lignes - partie 1, ce qui devrait peut-être éclaircir.

Le pseudo-code de la droite devrait donner une idée sur ce que l'algorithme Proto de la gauche fait. (Je vais en dire plus en bas sur les étranges pseudo-opérateur « matches »). La ligne 25 (à gauche) se lit comme suit : chaque terminal arg1 est remplacé par l'état. Tout comme est une sorte de marqueur, il est également proto::_state. It stands in for et reçoit l'argument d'État de Lambda. (Seulement la manière dont Proto parvient à transporter l'argument d'état à travers toutes les chaînes d'invocations et l'affiche -comme par magie- au bon endroit au bon moment est comme un mystère pour l'instant).

Maintenant que l'argument d'état est un tuple, nous devons modifier Lambda . Pour des espaces réservés, devrait être indexé dans le tuple pour retourner le bon argument ; devrait être remplacé par l'élément d'indice 0 du tuple, arg2 doit retourner le 1er élément, etc. Sinon, de façon générale, en pseudo-code on a :

 
Sélectionnez
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
// PSEUDO-CODE equivalent for the Lambda
// algorithm to the left.
auto Lambda( auto expr, auto state )
{
    // When evaluating the arg1 terminal,
    // return the state.
    if ( expr matches? argN ) return boost::get<N-1>(state);
    // Otherwise, do the "default" thing.
    // ...
}

Ci-dessus, le pseudo-code hand-wavy qui exprime notre intention : correspond au Nème espace réservé et retourne le (N-1)ème élément du tuple. Boost::get est une fonction qui récupère un élément d'un tuple. Nous voulons obtenir le (N-1)ème élément de ce tuple parce que arg1 correspond au 0ème élément du tuple, et ainsi de suite.

Cette partie devient compliquée, donc je vais d'abord montrer ce à quoi le code actuel ressemble puis décrire comment j'en suis arrivé là. Alors sans plus tarder, voici le nouvel algorithme Lambda (laissons tomber les qualifications proto:: dès maintenant pour plus de lisibilité) :

 
Sélectionnez
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
// A Proto algorithm for evaluating lambda
// expressions.
struct Lambda
  : or_<
        // When evaluating the argN terminal,
        // return the N-1 element of the state tuple. 
        when< argN_term< _ >, tuple_get(_value(_), _state) >
        // Otherwise, do the "default" thing.
      , otherwise< _default< Lambda > >
    >
{};

Sans aucun doute, ce code n'a pas de sens. Je n'ai pas encore dit ce que _ et _value sont (ils viennent de l'espace de nom proto::), ni montré l'implém

de tuple_get. Mais je suis certain que ce code est valide et fonctionne comme le pseudo-code ci-dessus. Cela signifie que la ligne 25 gère correctement tous les espaces réservés, et pas seulement arg1. Cela soulève deux questions :

  1. Comment est-ce que argN_term< _ > "correspond" convenablement à tous (et seulement) les espaces réservés ?
  2. Comment est-ce-que tuple_get(_value(_), _state) trouve et retourne l'élément correct du tuple ?

Mon utilisation de l'operateur inventé « assorti » dans mon pseudo-code aurait éveillé votre curiosité, surtout si vous êtes familier avec un langage fonctionnel, nous allons donc commencer par le point (1).

III. Pattern Matching

Quand je dis les mots « pattern matching », qu'est-ce-qui vient à l'esprit ? Les expressions régulières ? Les modèles de mariée ? Si vous êtes un programmeur fonctionnel, le terme « pattern matching » devrait retenir toute votre attention. Haskell, par exemple, peut distribuer à des branches différentes en appariant une valeur contre une série de patterns. Par exemple, nous allons écrire une fonction Haskell pour vérifier la réponse de la vie, l'univers et tout :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
-- Define a function "isTheAnswer" that returns
-- True if the argument is 42, False otherwise.
isTheAnswer 42 = True
isTheAnswer _  = False
 
main = do print (isTheAnswer 24)
          print (isTheAnswer 42)

Ce programme affiche:

 
Sélectionnez
True False

P vs NP

Il est plus facile de vérifier la réponse de la vie, l'univers et tout ce qui est à calculer.

On dirait que la fonction isTheAnswer est définie deux fois. Elle est vraiment définie une seule fois, mais avec des modèles différents. Le premier modèle est 42, qui correspond à lui-même. Le second modèle est _, qui correspond à n'importe lequel ; est le modèle nommé caractère générique. Voici comment fonctionne : partout où elle est invoquée, le runtime Haskell effectue un test : est-ce l'argument 42 ? Si c'est le cas, la première application est sélectionnée. Si non, il « passe » à la deuxième implémentation. Les modèles sont jugés dans l'ordre où ils apparaissent dans le code. (Fait intéressant, n'ayant pas de modèle correspondant à l'argument, ç'aurait été une erreur d'exécution, mais dans notre cas c'est impossible parce que le modèle générique agit comme un passe-partout.)

Le pattern matching en Haskell est un sujet riche et intéressant. Pour plus d'informations, consultez cette section sur le filtrage dans l'excellent Tutoriel Haskell pour les programmeurs C.

Pourquoi ai-je abordé Haskell dans tout cela ? Proto fait trop du pattern matching ! Regardez à nouveau Lambda, et cette fois-ci je vais remplacer otherwise à la ligne 27 par l'implémentation de son l'équivalent when :

 
Sélectionnez
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
// A Proto algorithm for evaluating lambda
// expressions.
struct Lambda
  : or_<
        // When evaluating the argN terminal,
        // return the N-1 element of the state tuple. 
        when< argN_term< _ >, tuple_get(_value(_), _state) >
        // Otherwise, do the "default" thing.
      , when< _, _default< Lambda > >
    >
{};

Lambda est un foncteur. Pour évaluer cette fonction, Proto utilise ple attern matching. Les modèles sont argN_term< _ > et _ sur les lignes 25 et 27. Proto les teste dans l'ordre dans lequel ils apparaissent dans le code, comme dans l'exemple de Haskell. Comme vous l'aurez deviné déjà, est le modèle générique de Proto qui correspond à tout.

Rappelons que les espaces réservés sont désormais définis comme suit :

 
Sélectionnez
typedef mpl::int_<0> nil_t;
typedef mpl::int_<1> one_t;
typedef mpl::int_<2> two_t;
 
lambda_expr< argN_term< nil_t >::type > const arg1 = {};
lambda_expr< argN_term< one_t >::type > const arg2 = {};
lambda_expr< argN_term< two_t >::type > const arg3 = {};

Lors de l'évaluation de avec arg1, Proto lie le type de arg1 avec les modèles de Lambda. En ignorant le wrapper lambda_expr (qui n'a aucun effet sur le pattern matching), le type de arg1 est argN_term< nil_t >::type. Ce type lui est lié en opposition à argN_term< _ >. Qu'est-ce-qui se passe ?

Rappelons que argN_term est défini comme suit:

 
Sélectionnez
template<typename Index>
struct argN_term
  : proto::terminal< argN_tag<Index> >
{};

Quel que soit les comportements que a comme modèle, il hérite de terminal . La classe terminal est également l'endroit où a imbriqué le typedef ::type. En fait, Proto demande réellement ce qui suit :

Est-ce que le type d'expression terminal< argN_tag< nil_t > >::type corresponde au modèle terminal< argN_tag< _ > > ?

La réponse est oui, mais pourquoi ? Ici, terminal remplit deux rôles. D'une part, il est utilisé pour générer un type d'expression (via de l'accès au typedef imbriqué ::type). D'autre part, il est utilisé comme un modèle correspondant à des types d'expression. Cette double nature de est un peu déroutant, mais il réduit le nombre de types nécessaires pour apprendre Proto.

Les constructeurs Haskell

Bartosz "Fermat" Milewski a trouvé une preuve vraiment merveilleux que la double nature du proto::terminal est analogue à l'utilisation secondaire de données Haskell constructeurs comme des modèles, mais cette marge est trop petite pour le contenir. Cherchez-le dans un futur article.

Mais encore nous nous demandons pourquoi partis en le type terminal< argN_tag< nil_t > >::type correspond au modèle terminal< argN_tag< _ > >. Eh bien, on est un terminal et l'autre est un modèle pour un terminal correspondre sondes Proto autre:.! Ne leurs types de valeurs correspondent? C'est, ne argN_tag< nil_t > correspondance argN_tag< _ > ? Oui, mais pourquoi? Proto a un ensemble de règles qui régissent la façon dont modèle produit assortis. Par exemple, n'importe quel type de valeur terminal lui-même correspond trivialement (par exemple int correspond int ), mais argN_tag<nil_t> et argN_tag<_> ne pas correspondre exactement.

Proto Pattern Matching

Pattern matching dans Proto est un sujet riche. Il est entièrement documenté ici , mais ce n'est pas une lecture facile. Nous verrons d'autres exemples de schémas Proto au cours de cette série d'articles.

La réponse est que Proto a une règle qui autorise les types de la valeur finale de la forme T< X > pour correspondre à des types comme T< _ > ; à savoir les cas de la même matrice où le modèle se les paramètres correspondent. Proto ne sait pas quelque chose de spécial à propos de argN_tag , mais il peut voir que argN_tag<nil_t> et argN_tag<_> conforme à cette règle, si le match réussit. Et enfin on peut expliquer pourquoi j'ai défini argN_tag la façon dont je l'ai fait: je voulais être en mesure de facilement motif correspond son type en utilisant cette règle.

De cette façon, tous les espaces réservés, arg1, arg2, et arg3, et d'autres que nous peut décider d'ajouter plus tard avoir des types qui correspondent au modèle argN_term< _ >.

IV. Types de fonctions comme Actions

Maintenant, qu'en est-il étrange tuple_get(_value(_), _state) qui est utilisé pour évaluer les espaces réservés lambda ? Il ressemble beaucoup à une expression, comme si nous faisons des appels de fonction, n'est-ce pas ? Cependant, il est utilisé comme un paramètre à la when modèle, de sorte qu'il ne peut pas être une expression. Il est, en fait, un type de fonction. Le when modèle interprète types de fonctions comme les appels de fonction. En d'autres termes, when utilise les types de fonctions en tant que langue spécifique à un domaine pour définir des actions. Voici comment lire cette proto-action-comme-un-fonction de type:

  1. Le type _ est un espace réservé à l'expression qui vient de correspondance.
  2. Le type _state est un espace réservé pour l'Etat.
  3. Tout de suite à gauche d'une parenthèse d'ouverture est un objet fonction TR1 style.
  4. Quelque chose entre parenthèses est un argument à l'objet de la fonction TR1 style.

En appliquant ces règles, proto::when tourne le type de fonction tuple_get(_value(_), _state) en un objet de fonction un peu comme le pseudo-code suivant :

 
Sélectionnez
// PSEUDO-CODE equivalent of tuple_get(_value(_), _state) 
auto eval_argN_term( auto expr, auto state ) 
{ 
    return tuple_get()(_value()(expr), state); 
}

Si vous voulez savoir comment when convertit les types de fonctions dans des objets fonctionnels équivalents, consultez le dernier article.

Ci-dessus, tuple_get() et _value() sont des objets de leurs types respectifs défaut construit. La génération de l'objet de fonction qui se passe entièrement au moment de la compilation. L'objet fonction est appelée lors de l'exécution en utilisant l'expression qui vient de correspondance et l'état actuel. En d'autres termes, eval_argN_term reçoit soit arg1, arg2 ou arg3 avec le tuple de l'Etat.

IV-A. Pourquoi _ reçoivent arg1 ?

Dans le modèle argN_term< _ >, le _ finit correspondant nil_t. Dans l'action, _ reçoit arg1 . Pourquoi ne pas recevoir nil_t, la chose elle correspondait ? Considérons la règle suivante à partir d'un algorithme Proto :

 
Sélectionnez
// What gets passed to foo?
when< plus<_,_>, foo(_) >

Dans le modèle, _ montre plus d'une fois, mais seulement une fois dans l'action. Qu'est-ce que _ signifie dans l'action? La seule chose sensée pour Proto à faire est de passer toute expression qui correspondait à la configuration (un binaire, plus l'expression) à foo . Par souci de cohérence, _ dans une action signifie toujours «toute expression qui vient de correspondance".

Maintenant tout ce qui reste à expliquer est le sens de _value et tuple_get . _value est assez facile: il s'agit d'un objet de fonction qui accepte bornes Proto et renvoie la valeur à l'intérieur. La valeur à l'intérieur arg1 est un objet de type argN_tag< nil_t > , de sorte que c'est ce qui est transmis à tuple_get avec le tuple. Et maintenant, le travail de tuple_get doit être clair: il doit retourner le bon élément d'un tuple donné le tuple et une instance de argN_tag .

IV-B. La plupart du code Confondre jamais?

Après avoir une action Proto comme tuple_get(yadda(yadda), yadda) lui a expliqué, un de mes collègues l'a décrite comme la pièce la plus déroutante de code qu'il ait jamais vu. Il ressemblait à quelque chose, mais il a trouvé que ce n'était pas la chose, mais il s'est avéré que c'était vraiment.

Dans le dernier article, j'ai fait une affaire pour les types de fonction d'être un peu langue spécifique au domaine de la composition de fonctions. Il en est de Proto. Autrefois considéré comme un peu de langue distincte, les actions Proto sont plus faciles à assimilez. À un certain moment vous renoncez à essayer de raisonner à travers comme C ++ et juste le voir comme Proto.

V. Détails, détails

Par rapport à la composition d'appariement de formes et de la fonction capiteux des choses, la tâche finale semble plutôt prosaïque: écrire un objet de fonction nommée tuple_get que, lorsqu'il est passé d'une instance de argN_tag et un tuple, retourne l'élément correct du tuple. Nous avons juste besoin d'envelopper le bon boost::get appel de fonction. Ce qui suit est un objet fonction TR1 style qui fait justement cela, dans toute sa gloire prosaïque. Explication à suivre.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
// A TR1-style function object that accepts an argN_tag
// instance and a tuple and returns the proper tuple element.
struct tuple_get : proto::callable
{
    // Nested result template for return type calculation
    template<typename Sig> struct result;
 
    template<typename This, typename ArgNTag, typename Tuple>
    struct result<This(ArgNTag const &, Tuple const &)>
    {
        // ArgNTag is an instance of argN_tag, which has a nested index typedef
        typedef typename ArgNTag::index N;
        // boost::tuples::element is a trait that returns the type of the Nth element
        typedef typename boost::tuples::element<N::value, Tuple>::type type;
    };
 
    template<typename ArgNTag, typename Tuple>
    typename result<tuple_get(ArgNTag const &, Tuple const &)>::type
    operator()(ArgNTag const &, Tuple const & tup) const
    {
        typedef typename ArgNTag::index N;
        // boost::get is a function that returns the Nth element of a tuple.
        return boost::get<N::value>(tup);
    }
};

Comme d'habitude avec les objets de la fonction TR1 de style, c'est un peu difficile de voir l'intention à travers tout le bruit syntaxique, mais là, il est sur ​​la ligne 23 : un appel à boost::get qui récupère un élément d'un tuple.

Certains détails méritent d'être mentionnés. tuple_get accepte comme premier argument une instance de argN_tag, qui, si vous regardez waaaaay revenir là où nous l'avons défini ci-dessus présente un typedef imbriquée appelée index . C'est un type entier enveloppé représentant un indice de tuple. entiers de type gainé comme mpl::int_ et std::integral_wrapper avoir une constante statique appelée imbriquée value qui (sans surprise) est la valeur de l'entier enveloppé. Donc mpl::int_<1>::value est 1 . Mais c'est une constante de compilation, on peut donc l'utiliser comme un paramètre de modèle de boost::get , ce qui est important. Quoi qu'il en soit, c'est là ::value provient des lignes 14 et 23.

Ligne 9 est la imbriquée spécialisée partielle result modèle, la façon idiomatique pour calculer le type de retour de tuple_get::operator() . Cette TR1 result_of chant et de danse doivent se sentir vaguement familier maintenant. Ligne 14 a la compilation équivalent du boost::get appel. boost::tuples::element est un trait qui prend un nombre entier de compilation et un type de tuple, et «revenus» le type de l'élément stocké à l' index.

Les tuples Boost vs les tuples C++ 0x

Le modèle en Boost appelé boost::tuples::element est connu comme std::tuple_element dans TR1 et C++ 0x. C'est la seule différence pratique.

La seule autre chose à noter dans le tuple_get mise en œuvre est l'héritage de proto::callable . Je n'ai pas montré assez sur actions Proto pour expliquer pourquoi cela est nécessaire. Pour l'instant, je vais juste dire ceci : tous les objets de la fonction TR1 style utilisés pour composer des actions en utilisant les types de fonctions (comme get_tuple(yadda(yadda), yadda) ) doit hériter de callable directement ou indirectement. Il aide Proto sens du type de fonction. Mais je vais vous épargner et de retarder l'explication complète pour un autre article.

VI. Récapitulatif

Ici, encore une fois, est la nouvelle version améliorée Lambda algorithme :

 
Sélectionnez
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
// A Proto algorithm for evaluating lambda
// expressions.
struct Lambda
  : or_<
        // When evaluating the argN terminal,
        // return the N-1 element of the state tuple. 
        when< argN_term< _ >, tuple_get(_value(_), _state) >
        // Otherwise, do the "default" thing.
      , when< _, _default< Lambda > >
    >
{};

Maintenant que nous savons ce qui se passe, nous pouvons apprécier la façon dont les parties travaillent ensemble: lors de la compilation la fonction envoi à l'aide de motifs, la composition de fonctions en utilisant les types de fonctions, et des objets de fonction TR1 de style. Chaque petit morceau, dit tuple_get, n'est pas difficile d'écrire ou assimilez à l'isolement. Les pièces s'emboîtent avec une syntaxe concise qui se sent naturel (ou viendra pour une sensation naturelle si vous restez avec lui). Au total, Lambda est assez puissant petit beastie; il peut évaluer les expressions lambda arbitrairement complexes avec un nombre quelconque d'espaces réservés arguments. Tout cela en 6 lignes de code. (OK, nous avons dû écrire un peu pour mettre en place ces 6 lignes, mais travailler avec moi.)

Cliquez ici pour voir la solution complète .

Je vous laisse avec un programme simple qui utilise notre bibliothèque lambda et l'expression lambda binaire arg1 + arg2 avec les deux entrées séquence std::transform algorithme - quoi d'autre? - Générer les nombres de Fibonacci. Amusez-vous.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
#include <algorithm>
 
int main()
{
    int data[10] = {0,1};
 
    // generate the Fibonacci sequence
    std::transform(data, data+8, data+1, data+2, arg1 + arg2);
 
    // write each element to std::cout
    std::for_each(data, data+10, std::cout << arg1 << ' ');
}

VII. Remerciements

VII-A. Auteur

Merci à Bartosz Milewski , Walter Bright , et Andrei Alexandrescu pour leurs précieux commentaires sur ce post.

VII-B. Developpez.com

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   



  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2014 Eric Niebler. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.