Boost logo

Boost :

Subject: Re: [boost] [Phoenix] Some questions and notes...
From: Joel de Guzman (joel_at_[hidden])
Date: 2008-09-25 21:06:10


Giovanni Piero Deretta wrote:

>> It's not broken. As Doug noted in his review, phoenix lambda
>> is like lambda protect (http://tinyurl.com/3sx7bo).
>
> I would be perfectly fine if it lambda[f] worked as protect(f), but it
> actually is subtly different
> What I do not like is the extra '()' you have to use to actually get
> the protected lambda:
>
> int i = 0;
> std::cout << protect(arg1)(i) ; // print 0

Have you tried it? I did and I get compiler error.
The result of protect(arg1)(i) is a lambda functor
as expected (and that's in line with the lambda docs).

     #include <boost/lambda/lambda.hpp>
     #include <iostream>

     int main()
     {
         using namespace boost::lambda;
         int i = 0;
         std::cout << protect(_1)(i) << std::endl;
         return 0;
     }

This:

     int xxx = protect(_1)(i);

gives me:

     'initializing' : cannot convert from
     'boost::lambda::lambda_functor<T>' to 'int'

> std::cout << lambda[arg1](i) ; // compile error
> std::cout << lambda[arg1]()(i) ; // ok, prints 1

Sure, and the correct lambda code is:

     std::cout << protect(_1)()(i) << std::endl;

Which prints 0.

> I now understand why you need another evaluation round, and I see the
> need for local variables in lambdas (I've missed them in boost.lambda,
> and it was one of the reasons I was eagerly waiting for phoenix to be
> reviewed).
>
> My only objection is that a lambda[f] which doesn't have any local
> variables should just return 'f' and not a nullary. In fact I think
> this should be a global propery of lambda expressions:
>
> Let 'add' be an binary lazy function:
>
> 'add(arg1, 0)'
>
> should return an unary function (as it is currently the case). OTOH:
>
> 'add(1, 2)'
>
> should immediately be evaluated and not return a nullary function. In
> practice, 'add' would be 'optionally lazy'. This is in fact not that
> surprising: let's substitute add with its corresponding operator:
>
> 'arg1 + 0'
>
> returns an unary funciton, but
>
> '1 + 2'
>
> is immediately evaluated. I know this is a bit controversial and would
> probably require large code changes, but probably a review is the best
> place to comment on design aspects.

IMO, it's not controversial. I've considered this approach a long
time ago. It's actually doable: evaluate immediately when there are no
placeholders in an expression. I'm not sure about the full effect
of this behavior, OTOH. Such things should be taken very carefully.
Mind you, the very impact of immediate evaluation on expressions like
above already confuses people. Sometimes, the effect is subtle
and is not quite obvious when you are dealing with complex
lambda expressions. I know, from experiences with ETs (prime
example is Spirit), that people get confused when an expression
is immediate or not. The classic example:

     for_each(f, l, std::cout << 123 << _1)

Oops! std::cout << 123 is immediate. But that's just for starters.
With some heavy generic code, you can't tell by just looking at the
code which expresions are being evaluated immediately or lazily.

> Anyways, I can live with the current design, 'optional lazyness' could
> be built on top of phoenix lazy functions. My only compliant is that
> the 'lambda[]' syntax is already taken.

I'd like to get convinced. Can you give me a nice use case
for this 'optional lazyness' thing that cannot be done with
the curent interface?

> BTW, FC++ had both 'optionally lazy' functions (which it called
> generalized currying) and required a top level 'lambda[]' around all
> lambdas.
>
>>> * non-standard result<> protocol. IMHO for Phoenix to become a first
>>> class boost library, it should support result_of out of the box. I
>>> understand that this is not done for backward compatibility, but I
>>> think that there are three solutions:
>>>
>>> 1) old users will still be able to use the Phoenix inside spirit if
>>> they have a large codebase that uses the old protocol.
>>> 2) phoenix could provide a wrapper that converts from the old to the
>>> new protocol and viceversa. This would require some changes in the
>>> client code.
>>> 3) phoenix could detect the protocol used by the user function and
>>> switch between the old and new 'result'. This is hard to do in a
>>> robust way, but breaks no user code.
>>>
>>> I would prefer the first option.
>> What's the first solution? It's not clear.
>>
>
> Completely ditch the old protocol and adopt the result_of one. Users
> that need backward compatibility can still use the phoenix inside
> spirit.

Hmmm.. That's a good suggestion.

>>> * operator->*. I sometimes use this operator with boost lambda, as a
>>> short hand for binding member functions, but, as in phoenix, it
>>> requires it lhs to be a pointer. Would it possible to extend it to any
>>> functions? for example:
>>>
>>> struct foo {
>>> int bar;
>>> };
>>>
>>> std::vector<foo> in =...;
>>> std::vector<int> out;
>>>
>>> std::transform(in.begin(), in.end(), std::back_inserter(out),
>>> (&arg1)->*&foo::bar);
>>>
>>> The parenthesis, and ampersand are ugly, it would be great if this worked:
>>>
>>> std::transform(in.begin(), in.end(), std::back_inserter(out),
>>> arg1->*&foo::bar);
>> arg1->*&foo::bar is perfectly allowed! Why do you think it's not?
>
> Sure it does, but the placeholder must be substituted with a pointer...
>
>> see /test/operator/member.cpp for examples on this. Some
>> examples:
>>
>> Test test = {1};
>> const Test* cptr = &test;
>> Test* ptr = &test;
>>
>> BOOST_TEST((val(ptr)->*&Test::value)() == 1);
>
> This doesn't compile
> BOOST_TEST((arg1->*&Test::value)(test) == 1);
>
> because 'test' is not a pointer. OTOH boost::bind(&Test::value,
> arg1)(x) compiles for both x of type "Test&" and "Test*".

Ah! Good point. But, hey, doesn't -> imply "pointer"? bind
OTOH does not imply pointer. But sure I get your point and it's
easy enough to implement. Is this a killer feature for you?

> BTW, what if the member function is not nullary?
>
> struct foo {
> void bar(int){}
> };
>
> foo * x =...;
> int y = 0;
> ((arg1->*&foo::bar)(arg2))(x, y);
>
> The above (or any simple variation I could think of) doesn't compile.
> There is a way to make something like this to work without using bind?
> Not that it is very compelling, I'm just curious.

The examples show how. I don't know why your example does not compile.
Again, see /test/operator/member.cpp for examples on this. Here's
one that's not nullary:

     struct Test
     {
         int func(int n) const { return n; }
     };

...

     BOOST_TEST((val(ptr)->*&Test::func)(3)() == 3);

and I just added this test for you:

     int i = 33;
     BOOST_TEST((arg1->*&Test::func)(arg2)(cptr, i) == i);

Compiles fine.

>>> Well, that's all for now, those questions are mostly to get the
>>> discussion rolling, more to come.
>
> An additional question: is it possible to make lambdas always
> assignable? ATM, if you close around a reference (something which I
> think is very common), the lambda expression is only

Not sure what you mean by "close around a reference".

> CopyConstructible. This means that an iterator that holds a lambda
> expression by value internally (think about filter_iterator or
> transform iterator) technically does no longer conform to the Iterator
> concept.

Good point.

> A simple fix, I think, is internally converting all references to
> reference wrappers which are assignable.

I wish it was that simple. Anyway, gimme some time to ponder on
the impact of this on the implementation. Perhaps an easier way
is to do as ref does: store references by pointer.

Regards,

-- 
Joel de Guzman
http://www.boostpro.com
http://spirit.sf.net

Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk