Boost logo

Boost :

Subject: Re: [boost] [Phoenix] Some questions and notes...
From: Giovanni Piero Deretta (gpderetta_at_[hidden])
Date: 2008-09-25 09:45:12


On Thu, Sep 25, 2008 at 2:43 PM, Joel de Guzman
<joel_at_[hidden]> wrote:
> Giovanni Piero Deretta wrote:
>> Hi all,
>>
>> I'm writing a review of phoenix, but first I have a some questions and
>> notes.
>>
>> * First of all, it is not clear to me how 'lambda' works. I would have
>> thought that it just introduced a new scope, but it seems that it does
>> something more:
>>
>> 1: int i = 10;
>> 2: cout << arg1(i) <<endl;
>> 3: cout << lambda[ arg1 ](i) <<endl; // doesn't compile
>> 4: cout << lambda[ arg1 ]()(i)
>>
>> The use of lambda at line 3 is completely superfluous, nevertheless, I
>> prefer that syntax because it makes it clear that I'm using a lambda
>> expression (always using lambda will also allow some other tricks I'll
>> discuss later). Surprisingly, line 3 doesn't compile.
>> lambda::operator[] actually returns a nullary stub that must be
>> evaluated to actually get our unary function. Is this really
>> necessary?
>
> Yes.
>
>> can it be fixed?
>
> 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
std::cout << lambda[arg1](i) ; // compile error
std::cout << lambda[arg1]()(i) ; // ok, prints 1

> lambda[...] returns another lambda functor, hence for
> each lambda, there's one function application.
> Some samples from the tests:
>
<snipped examples>

>
> So, walking through the first example (which resembles yours):
>
> lambda[_1]()(x);
>
> The first call (empty) does no substitution and results
> to:
>
> _1
>
> The second call with x then substitutes x for _1.
>
> Let's take another example (2):
>
> (_1 + lambda[_1 + 2])(x)(y)
>
> The first call (with x) results in another lambda functor
> after substituting the outer _1 for x and exposing the
> expression inside the lambda without substitution:
>
> (x + _1 + 2)
>
> The second call (with y) then does the final invocation:
>
> (x + y + 2)
>
> The difference with lambda's protect is that phoenix' lambda
> has true local variables and with the mechanism, inner lambda
> scopes can actually get arguments and other information from
> outer lambda scopes. This is important. For instance, the
> example in the doc:

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.

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.

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.

>> * Perfect forwarding. I'm used to the perfect forwarding in boost
>> lambda (which, contrary to what the documentation states, perfectly
>> forwards up to 3 args). Personally have little use for lambdas that
>> take their arguments by non const reference as I strive for
>> referential transparency.
>
> We will have perfect forwarding controlled by a macro.

Great!

<snip>
>
>> * 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*".

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.

>
>> 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
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.

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

-- 
gpd

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