Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext
Separating Grammars And Transforms
[Note] Note

This is an advanced topic that is only necessary for people defining large EDSLs. Feel free to skip this if you're just getting started with Proto.

So far, we've seen examples of grammars with embedded transforms. In practice, grammars can get pretty large, and you may want to use them to drive several different computations. For instance, you may have a grammar for a linear algebra domain, and you may want to use it to compute the shape of the result (vector or matrix?) and also to compute the result optimally. You don't want to have to copy and paste the whole shebang just to tweak one of the embedded transforms. What you want instead is to define the grammar once, and specify the transforms later when you're ready to evaluate an expression. For that, you use external transforms. The pattern you'll use is this: replace one or more of the transforms in your grammar with the special placeholder proto::external_transform. Then, you'll create a bundle of transforms that you will pass to the grammar in the data parameter (the 3rd parameter after the expression and state) when evaluating it.

To illustrate external transforms, we'll build a calculator evaluator that can be configured to throw an exception on division by zero. Here is a bare-bones front end that defines a domain, a grammar, an expression wrapper, and some placeholder terminals.

#include <boost/assert.hpp>
#include <boost/mpl/int.hpp>
#include <boost/fusion/container/vector.hpp>
#include <boost/fusion/container/generation/make_vector.hpp>
#include <boost/proto/proto.hpp>
namespace mpl = boost::mpl;
namespace proto = boost::proto;
namespace fusion = boost::fusion;

// The argument placeholder type
template<typename I> struct placeholder : I {};

// The grammar for valid calculator expressions
struct calc_grammar
  : proto::or_<
        proto::terminal<placeholder<proto::_> >
      , proto::terminal<int>
      , proto::plus<calc_grammar, calc_grammar>
      , proto::minus<calc_grammar, calc_grammar>
      , proto::multiplies<calc_grammar, calc_grammar>
      , proto::divides<calc_grammar, calc_grammar>
    >
{};

template<typename E> struct calc_expr;
struct calc_domain : proto::domain<proto::generator<calc_expr> > {};

template<typename E>
struct calc_expr
  : proto::extends<E, calc_expr<E>, calc_domain>
{
    calc_expr(E const &e = E()) : calc_expr::proto_extends(e) {}
};

calc_expr<proto::terminal<placeholder<mpl::int_<0> > >::type> _1;
calc_expr<proto::terminal<placeholder<mpl::int_<1> > >::type> _2;

int main()
{
    // Build a calculator expression, and do nothing with it.
    (_1 + _2);
}

Now, let's embed transforms into calc_grammar so that we can use it to evaluate calculator expressions:

// The calculator grammar with embedded transforms for evaluating expression.
struct calc_grammar
  : proto::or_<
        proto::when<
            proto::terminal<placeholder<proto::_> >
          , proto::functional::at(proto::_state, proto::_value)
        >
      , proto::when<
            proto::terminal<int>
          , proto::_value
        >
      , proto::when<
            proto::plus<calc_grammar, calc_grammar>
          , proto::_default<calc_grammar>
        >
      , proto::when<
            proto::minus<calc_grammar, calc_grammar>
          , proto::_default<calc_grammar>
        >
      , proto::when<
            proto::multiplies<calc_grammar, calc_grammar>
          , proto::_default<calc_grammar>
        >
      , proto::when<
            proto::divides<calc_grammar, calc_grammar>
          , proto::_default<calc_grammar>
        >
    >
{};

With this definition of calc_grammar we can evaluate expressions by passing along a Fusion vector containing the values to use for the _1 and _2 placeholders:

int result = calc_grammar()(_1 + _2, fusion::make_vector(3, 4));
BOOST_ASSERT(result == 7);

We also want an alternative evaluation strategy that checks for division by zero and throws an exception. Just how ridiculous would it be to copy the entire calc_grammar just to change the one line that transforms division expressions?! External transforms are ideally suited to this problem.

First, we give the division rule in our grammar a "name"; that is, we make it a struct. We'll use this unique type later to dispatch to the right transforms.

struct calc_grammar;
struct divides_rule : proto::divides<calc_grammar, calc_grammar> {};

Next, we change calc_grammar to make the handling of division expressions external.

// The calculator grammar with an external transform for evaluating
// division expressions.
struct calc_grammar
  : proto::or_<
        /* ... as before ... */
      , proto::when<
            divides_rule
          , proto::external_transform
        >
    >
{};

The use of proto::external_transform above makes the handling of division expressions externally parameterizeable.

Next, we use proto::external_transforms<> (note the trailing 's') to capture our evaluation strategy in a bundle that we can pass along to the transform in the data parameter. Read on for the explanation.

// Evaluate division nodes as before
struct non_checked_division
  : proto::external_transforms<
        proto::when< divides_rule, proto::_default<calc_grammar> >
    >
{};

/* ... */

non_checked_division non_checked;
int result2 = calc_grammar()(_1 / _2, fusion::make_vector(6, 2), non_checked);

The struct non_cecked_division associates the transform proto::_default<calc_grammar> with the divides_rule grammar rule. An instance of that struct is passed along as the third parameter when invoking calc_grammar.

Now, let's implement checked division. The rest should be unsurprising.

struct division_by_zero : std::exception {};

struct do_checked_divide : proto::callable
{
    typedef int result_type;
    int operator()(int left, int right) const
    {
        if (right == 0) throw division_by_zero();
        return left / right;
    }
};

struct checked_division
  : proto::external_transforms<
        proto::when<
            divides_rule
          , do_checked_divide(calc_grammar(proto::_left), calc_grammar(proto::_right))
        >
    >
{};

/* ... */

try
{
    checked_division checked;
    int result3 = calc_grammar_extern()(_1 / _2, fusion::make_vector(6, 0), checked);
}
catch(division_by_zero)
{
    std::cout << "caught division by zero!\n";
}

The above code demonstrates how a single grammar can be used with different transforms specified externally. This makes it possible to reuse a grammar to drive several different computations.

Separating Data From External Transforms

As described above, the external transforms feature usurps the data parameter, which is intended to be a place where you can pass arbitrary data, and gives it a specific meaning. But what if you are already using the data parameter for something else? The answer is to use a transform environment. By associating your external transforms with the proto::transforms key, you are free to pass arbitrary data in other slots.

To continue the above example, what if we also needed to pass a piece of data into our transform along with the external transforms? It would look like this:

int result3 = calc_grammar_extern()(
    _1 / _2
  , fusion::make_vector(6, 0)
  , (proto::data = 42, proto::transforms = checked)
);

In the above invocation of the calc_grammar_extern algorithm, the map of external transforms is associated with the proto::transforms key and passed to the algorithm in a transform environment. Also in the transform environment is a key/value pair that associates the value 42 with the proto::data key.


PrevUpHomeNext