![]() |
Home | Libraries | People | FAQ | More |
![]() |
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.
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.