[Proto?][Phoenix?] : Advice on creating lazy data structures

With Phoenix or lambda-like DSELs implemented with Proto, it is easy to implement stuff when the expected actual parameters to the lambda expressions are primitive types. When I want to pass arbitrary types to the lambda expression and manipulate it in the lambda expression, things become complex. For example, consider the following function : struct point { float x; float y; }; float sum(point p) { return p.x+p.y; } If I want to have a lazy version of this function, I cannot simply do (arg1.x + arg1.y)(p). I have to implement functions that return Proto or Phoenix expressions and use it in the language. In the above case, it would look something like (point_get_x_(arg1) + point_get_y_(arg2))(p). So for every type I want to support in the DSEL, I have to implement lazy versions of functions to get and set the type members, or call some methods. In other words, the language has to be aware of all the data types that can be used with it. I am looking for some advice on a standard or a generic way for creating such lazy data structures. Has something like this been done before? I will be grateful for any pointers and suggestions. Thanks, Manjunath

On 3/30/2010 2:02 PM, Manjunath Kudlur wrote:
With Phoenix or lambda-like DSELs implemented with Proto, it is easy to implement stuff when the expected actual parameters to the lambda expressions are primitive types. When I want to pass arbitrary types to the lambda expression and manipulate it in the lambda expression, things become complex. For example, consider the following function :
struct point { float x; float y; };
float sum(point p) { return p.x+p.y; }
If I want to have a lazy version of this function, I cannot simply do (arg1.x + arg1.y)(p). I have to implement functions that return Proto or Phoenix expressions and use it in the language. In the above case, it would look something like (point_get_x_(arg1) + point_get_y_(arg2))(p). So for every type I want to support in the DSEL, I have to implement lazy versions of functions to get and set the type members, or call some methods. In other words, the language has to be aware of all the data types that can be used with it. <snip>
Not necessarily. These these can be added to your DSEL post-hoc, and there are some fun games you can play with proto's operator->*. Consider the following program: #include <cassert> #include <boost/proto/proto.hpp> namespace proto = boost::proto; struct point { int x, y; }; proto::terminal<int point::*>::type x = {&point::x}; proto::terminal<int point::*>::type y = {&point::y}; struct arg_ {}; proto::terminal<arg_>::type arg = {{}}; struct micro_lambda : proto::or_< proto::when< proto::terminal<arg_> , proto::_state > , proto::otherwise< proto::_default<micro_lambda> > > {} eval; int main() { point p = {2,4}; int r = eval( arg->*x + arg->*y, p ); assert( r == p.x + p.y ); } You can't overload operator dot (.), but proto makes up for it by automatically interpreting ->* as if it were dot. Proto evaluates the LHS and RHS, and as long as the RHS is something like a data member pointer or member function pointer that can be applied to the LHS, it all Just Works. It works with mutation through the data member pointer, too: eval( arg->*x *= 21, p ); assert( p.x == 42 ); HTH, -- Eric Niebler BoostPro Computing http://www.boostpro.com

<snip>
Not necessarily. These these can be added to your DSEL post-hoc, and there are some fun games you can play with proto's operator->*. Consider the following program:
Thanks, Eric. That was an Ah-ha moment for me. I tried a different variation of your program. I changed point to a templated tuple<typename X, typename Y> and declared the terminals x and y with proto::_ template arguments. Here is the complete program : #include <cassert> #include <boost/proto/proto.hpp> #include <iostream> namespace proto = boost::proto; template<typename X, typename Y> struct tuple { X x; Y y; }; template<typename X, typename Y> struct tuple_types { typedef X tuple<X, Y>::* X_type; typedef Y tuple<X, Y>::* Y_type; }; //Does not work // proto::terminal<tuple_types<proto::_, proto::_>::X_type>::type x // = {&tuple<proto::_, proto::_>::x}; // proto::terminal<tuple_types<proto::_, proto::_>::Y_type>::type y // = {&tuple<proto::_, proto::_>::y}; proto::terminal<tuple_types<int, float>::X_type>::type x = {&tuple<int, float>::x}; proto::terminal<tuple_types<int, float>::Y_type>::type y = {&tuple<int, float>::y}; struct arg_ {}; proto::terminal<arg_>::type arg = {{}}; struct micro_lambda : proto::or_< proto::when< proto::terminal<arg_> , proto::_state
, proto::otherwise< proto::_default<micro_lambda>
{} eval; using namespace std; int main() { tuple<int, float> t; t.x = 1; t.y = 41.f; cout << eval(arg->*x + arg->*y, t) << endl; } When I declare x and y like shown in the commented out part, I get compile errors. What am I doing wrong there? Do I need extra grammar rules to match the member pointers of a templated class? Manjunath

Thanks, Eric. That was an Ah-ha moment for me. I tried a different variation of your program. I changed point to a templated tuple<typename X, typename Y> and declared the terminals x and y with proto::_ template arguments. Here is the complete program :
I re-worked the program as below to achieve what I need. But now, using the DSEL with a new data-type becomes much more invasive, because I need some rules to the grammar. May be there is a less invasive and more elegant way? Also, I don't understand why I need both This(tuple<>&) and This(const tuple<>&) versions in the get_tuple_* classes. Removing either one of them causes a compiler error. #include <cassert> #include <boost/proto/proto.hpp> #include <iostream> namespace proto = boost::proto; template<typename X, typename Y> struct tuple { X x; Y y; }; struct arg_ {}; proto::terminal<arg_>::type arg = {{}}; struct x_ {}; proto::terminal<x_>::type x = {{}}; struct y_ {}; proto::terminal<y_>::type y = {{}}; struct get_tuple_x : proto::callable { template<typename Sig> struct result; template<typename This, typename X, typename Y> struct result<This(tuple<X, Y> &)> { typedef X type; }; template<typename This, typename X, typename Y> struct result<This(const tuple<X, Y> &)> { typedef X type; }; template<typename X, typename Y> X operator()(tuple<X, Y> &t) const { return t.x; } template<typename X, typename Y> X operator()(const tuple<X, Y> &t) const { return t.x; } }; struct get_tuple_y : proto::callable { template<typename Sig> struct result; template<typename This, typename X, typename Y> struct result<This(tuple<X, Y> &)> { typedef Y type; }; template<typename This, typename X, typename Y> struct result<This(const tuple<X, Y> &)> { typedef Y type; }; template<typename X, typename Y> Y operator()(tuple<X, Y> &t) const { return t.y; } template<typename X, typename Y> Y operator()(const tuple<X, Y> &t) const { return t.y; } }; struct micro_lambda : proto::or_< proto::when< proto::mem_ptr<proto::terminal<arg_>, proto::terminal<x_> > , get_tuple_x(proto::_state)> , proto::when< proto::mem_ptr<proto::terminal<arg_>, proto::terminal<y_> > , get_tuple_y(proto::_state)> , proto::otherwise< proto::_default<micro_lambda>
{} eval; using namespace std; int main() { tuple<int, float> t; t.x = 1; t.y = 41.f; cout << eval(arg->*x + arg->*y, t) << endl; } Manjunath

On 3/30/2010 12:48 PM, Manjunath Kudlur wrote:
Thanks, Eric. That was an Ah-ha moment for me. I tried a different variation of your program. I changed point to a templated tuple<typename X, typename Y> and declared the terminals x and y with proto::_ template arguments. Here is the complete program :
<snip> Sorry for the delay. I've been away from my computer. This is indeed a tricky problem. The trick is to realize that grammars can be made open and extensible in a variety of ways. The simple is to define a customization point. What you need is a way to plug into an existing grammar new classes and members. Define a get_member_ptr template that users of your grammar can use to make it aware of third-party types. Code attached below: #include <cassert> #include <iostream> #include <boost/proto/proto.hpp> namespace proto = boost::proto; using proto::_; template<typename T> struct type2type {}; template<typename T> struct tag {}; template<typename T, typename MemTag> struct get_mem_ptr; struct get_mem_ptr_wrap : proto::callable { template<typename Sig> struct result; template<typename This, typename T, typename MemTag> struct result<This(type2type<T>, tag<MemTag> const &)> : boost::result_of<get_mem_ptr<T, MemTag>()> {}; template<typename T, typename MemTag> typename boost::result_of<get_mem_ptr<T, MemTag>()>::type operator()(type2type<T>, tag<MemTag> const &) const { return get_mem_ptr<T, MemTag>()(); } }; template<typename X, typename Y> struct pair { X x; Y y; }; struct x_tag {}; struct y_tag {}; template<typename X, typename Y> struct get_mem_ptr<pair<X, Y>, x_tag> { typedef X pair<X, Y>::*result_type; result_type operator()() const { return &pair<X, Y>::x; } }; template<typename X, typename Y> struct get_mem_ptr<pair<X, Y>, y_tag> { typedef Y pair<X, Y>::*result_type; result_type operator()() const { return &pair<X, Y>::y; } }; proto::terminal<tag<x_tag> >::type const x = {{}}; proto::terminal<tag<y_tag> >::type const y = {{}}; struct arg_ {}; proto::terminal<arg_>::type arg = {{}}; struct micro_lambda : proto::or_< proto::when< proto::mem_ptr<_, proto::terminal<tag<_> > > , micro_lambda( proto::_make_mem_ptr( proto::_left , proto::_make_terminal( get_mem_ptr_wrap( type2type<micro_lambda(proto::_left)>() , proto::_value(proto::_right) ) ) ) ) > , proto::when< proto::terminal<arg_> , proto::_state > , proto::otherwise< proto::_default<micro_lambda> > > {} eval; int main() { pair<int, float> p = {2,4.f}; float r = eval( arg->*x + arg->*y, p ); assert( r == p.x + p.y ); std::cout << r << std::endl; eval( arg->*x *= 21, p ); assert( p.x == 42 ); std::cout << p.x << std::endl; } Note that the x and y globals are no longer raw member pointer terminals but tags. The grammar recognizes tags and uses them to look up the actual member pointer. It also needs to know the class type, so that is calculated and also passed along. This code runs compiles and runs fine with gcc. Msvc doesn't like it. But maybe you can sort that out. -- Eric Niebler BoostPro Computing http://www.boostpro.com
participants (2)
-
Eric Niebler
-
Manjunath Kudlur