Making C++ more functional

219 views
Skip to first unread message

Xeo

unread,
Apr 23, 2013, 4:03:19 AM4/23/13
to std-pr...@isocpp.org
(Formerly called "Lifting overload sets into function objects", but that describes just one of the things it does, not what it brings, so I changed the name.)
Linked below is an updated version of N3617, a proposal to open C++ up to more functional programming. The idea is to add a language feature that allows what functional languages naturally have - passing functions by their name.

Discussions and ideas are more than welcome.

(Note: I deleted the prior post because I made a mistake and left the Nnnnn in the updated document.)
functional_cpp.html

Nikolay Ivchenkov

unread,
Apr 24, 2013, 4:29:29 AM4/24/13
to std-pr...@isocpp.org
It's good to see that we have an official proposal about forwarding
lambdas. I was going to write similar paper, but now we can just
discuss this one.

> When dealing with generic algorithms, like the function templates in
> <algorithm>, it can be quite cumbersome to pass an overloaded
> function or a function template

... or a function with default arguments:

    std::wstring to_wstring(
        std::string const &s,
        encoding_t encoding = default_encoding());

    std::vector<std::string> src = source();
    std::vector<std::wstring> dst;

    std::transform(
        src.begin(),
        src.end(),
        std::back_inserter(dst),
        to_wstring); // won't work

In some cases we can't safely use address of a member function (even
with further explicit conversions), because the function's type is not
fixed: according to C++11 - 17.6.5.5,

    An implementation may declare additional non-virtual member
    function signatures within a class:

        — by adding arguments with default values to a member function
          signature; [Footnote: Hence, the address of a member
          function of a class in the C++ standard library has an
          unspecified type.] [ Note: An implementation may not add
          arguments with default values to virtual, global, or
          non-member functions.—end note ]

        — by replacing a member function signature with default values
          by two or more member function signatures with equivalent
          behavior; and

        — by adding a member function signature for a member function
          name.

In general, we can take address of only those functions which are
guarateed to have the same type (and also be non-overloaded
non-templates if we don't use further casts) forever, but such promise
may be too strong in many cases, so we need something more flexible
than the built-in address-of operator. Short forwarding lambdas would
be very helpful.

> [](auto&&... vs)
> { return id-expression(std::forward<decltype(vs)>(vs)...); }
>
> The above allows the user to pass any overloaded function or
> function template as if it were a function object, bypassing the
> main issues mentioned in the motivation. However, there are
> drawbacks:
>
> * Writing out the whole lambda expression is tedious and introduces
>   unnecessary clutter.

We could have more short notation:

    forwarding-lambda-expression:
        lambda-introducer lambda-declarator opt =>
            assignment-expression

    assignment-expression:
        forwarding-lambda-expression
        ....

    [](auto&&... vs) => id-expression(FORWARD(vs)...);

but

    [] id-expression

is obviously more concise.

> * It only allows exactly that invocation form and dismisses member
>   functions and member data.

I consider that as an advantage. If we want to use a class member, we
should express our intent explicitly. Moreover, an access through .
(dot) should be distinguishable from an access through -> (arrow).
For example,

    x.reset()

and

    x->reset()

may both be well-formed but have different meaning.

> * Operator support is incomplete, only allowing the non-member
>   direct invocation syntax operator@(arguments...), ignoring member
>   operator overloads as well as the special rules for operator
>   function name look-up

I don't see problem here. We are free to change lambda definition
accordingly.

    [](auto &&x) => @FORWARD(x)
    [](auto &&x) => FORWARD(x)@
    [](auto &&x, auto &&y) => (FORWARD(x) @ FORWARD(y))

> These drawbacks, especially the third one, can not be solved in a
> "library"-way (say, with a macro).

That's a controversial opinion. We could use several macro
definitions:

    #define FORWARD(x) static_cast<decltype(x) &&>(x)

    #define FUNC(f) \
        [](auto &&... params) => f(FORWARD(params)...)

    #define MEM_FUNC(f) \
        [](auto &&obj, auto &&... params) => \
            FORWARD(obj).f(FORWARD(params)...)

    #define INDIRECT_MEM_FUNC(f) \
        [](auto &&obj, auto &&... params) => \
            FORWARD(obj)->f(FORWARD(params)...)

    #define UNARY_OP(op) \
        [](auto &&x) => op FORWARD(x)

    #define POSTFIX_OP(op) \
        [](auto &&x) => FORWARD(x) op

    #define BINARY_OP(op) \
        [](auto &&x, auto &&y) => (FORWARD(x) op FORWARD(y))

    #define SUBSCRIPT_OP \
        [](auto &&x, auto &&y) => FORWARD(x)[FORWARD(y)]

    #define CALL_OP \
        [](auto &&obj, auto &&... params) => \
            FORWARD(obj)(FORWARD(params)...)

I would still prefer core language solution though :-)

> The semantics of INVOKE seemed like a good starting point

From my point of view, contrived uniformity is an evil. I can assume
that fast-to-write-but-hard-to-read programs are what some people
really want, but I don't fall into this category. If we need to waste
a lot of time in order to figure out what exactly some simple code is
supposed to do, because we have to thoroughly investigate large context
and apply several disambiguation rules, then visual simplicity and
uniformity don't look so cute.

I would prefer to have different syntax for different use cases,
rather than to deal with dirty tricks like INVOKE:

1) []id_expression
    
    [](auto &&... params) => id_expression(FORWARD(params)...)

2) [].id_expression
    
    [](auto &&x, auto &&... params) =>
        FORWARD(x).id_expression(FORWARD(params)...)

3) []->id_expression
    
    [](auto &&x, auto &&... params) =>
        FORWARD(x)->id_expression(FORWARD(params)...)

4) [] = .id_expression
    
    [](auto &&x) => FORWARD(x).id_expression

5) [] = ->id_expression
    
    [](auto &&x) => FORWARD(x)->id_expression

Advantages:

* this approach is less error-prone, because we explicitly state what
  we want exactly, while implicit assumptions made by a too smart
  compiler have more chances to be wrong (i.e. not match original
  intent)

    auto m = [] = .run;
    auto c = [].run;    

    struct F
    {
        std::function<void()> run;
    } f;

    m(f) = []{ /*...*/  }; // assigns []{ /*...*/  } to f.run
    c(f); // calls f.run()

* the semantics is transparent for readers;

* simple correspondence would (potentially) help compilers to provide
  better diagnostic messages, because if our code is ill-formed, we
  don't need (potentially long) explanation why 5 different
  interpretations are wrong / ill-formed.

> An idea was to allow []obj.id-expression for this-binding (like
> std::bind) - []x.foo would then create a nullary lifting-lamba.

I think, it should be [=]x.foo (x is captured by copy) or [&]x.foo
(x is captured by reference).

Giovanni Piero Deretta

unread,
Apr 24, 2013, 12:05:43 PM4/24/13
to std-pr...@isocpp.org

On Wednesday, April 24, 2013 9:29:29 AM UTC+1, Nikolay Ivchenkov wrote:
It's good to see that we have an official proposal about forwarding
lambdas. I was going to write similar paper, but now we can just
discuss this one.


[snip]
> * It only allows exactly that invocation form and dismisses member
>   functions and member data.

I consider that as an advantage. If we want to use a class member, we
should express our intent explicitly. Moreover, an access through .
(dot) should be distinguishable from an access through -> (arrow).
For example,

    x.reset()

and

    x->reset()

may both be well-formed but have different meaning.

[snip]
> The semantics of INVOKE seemed like a good starting point

From my point of view, contrived uniformity is an evil. I can assume
that fast-to-write-but-hard-to-read programs are what some people
really want, but I don't fall into this category. If we need to waste
a lot of time in order to figure out what exactly some simple code is
supposed to do, because we have to thoroughly investigate large context
and apply several disambiguation rules, then visual simplicity and
uniformity don't look so cute.


The uniformity is not just for its own sake. It is vital for generic code. Think for example about std::begin/end which exist to bridge the gap between containers having member functions and other ranges which do not have them.

There has been a lot of talks in the past about unifying the function call syntax, but nothing ever materialized. This proposal does it, plus a few other useful additions.

-- gpd

Nikolay Ivchenkov

unread,
Apr 24, 2013, 1:16:04 PM4/24/13
to std-pr...@isocpp.org
---------- Forwarded message ----------
From: Xeo <...>
Date: Wed, Apr 24, 2013 at 1:16 PM
Subject: Re: Making C++ more functional


On Wednesday, April 24, 2013 10:29:29 AM UTC+2, Nikolay Ivchenkov wrote:

... or a function with default arguments:
 
Ah, I knew I forgot something in my list. Also, I'll steal that example.
 
In some cases we can't safely use address of a member function (even
with further explicit conversions), because the function's type is not
fixed: according to C++11 - 17.6.5.5 

I didn't know that, another good example!

> * It only allows exactly that invocation form and dismisses member
>   functions and member data.

I consider that as an advantage. If we want to use a class member, we
should express our intent explicitly. Moreover, an access through .
(dot) should be distinguishable from an access through -> (arrow).
For example,

    x.reset()

and

    x->reset()

may both be well-formed but have different meaning.

A good point. And pulling up something further down:


> The semantics of INVOKE seemed like a good starting point

From my point of view, contrived uniformity is an evil. I can assume
that fast-to-write-but-hard-to-read programs are what some people
really want, but I don't fall into this category.

As I said, it was a *starting point*. It wasn't meant to be the end of it all. I just had to start somewhere.
 
I don't see problem here. We are free to change lambda definition
accordingly.

    [](auto &&x) => @FORWARD(x)
    [](auto &&x) => FORWARD(x)@
    [](auto &&x, auto &&y) => (FORWARD(x) @ FORWARD(y))

I don't quite get what exactly you're on to? (Also, is FORWARD supposed to be a `#define FORWARD(x) std::forward<decltype(x)>(x)`? If yes -> sad panda.)

I would prefer to have different syntax for different use cases,
rather than to deal with dirty tricks like INVOKE:

A thought that was lingering in my mind was to let this also enable a uniform calling syntax, but in hindsight that may be conflating unrelated concepts.
 
I think, it should be [=]x.foo (x is captured by copy) or [&]x.foo
(x is captured by reference).

What about [x].foo or [&x].foo? :) Just food for thought.

Nikolay Ivchenkov

unread,
Apr 24, 2013, 1:22:53 PM4/24/13
to std-pr...@isocpp.org
On Wed, Apr 24, 2013 at 1:16 PM, Xeo <...> wrote:

I don't see problem here. We are free to change lambda definition
accordingly.

    [](auto &&x) => @FORWARD(x)
    [](auto &&x) => FORWARD(x)@
    [](auto &&x, auto &&y) => (FORWARD(x) @ FORWARD(y))

I don't quite get what exactly you're on to?

I want to say that an operator invocation (as well as an operator
function invocation of any form) can be wrapped in a polymorphic
lambda with explicitly provided parameters. The resulting expression
would be far from being terse, but we can get desirable functionality;
therefore, I don't see a problem in this part:


    "Operator support is incomplete, only allowing the non-member
    direct invocation syntax operator@(arguments...), ignoring member
    operator overloads as well as the special rules for operator
    function name look-up."
 
(Also, is FORWARD supposed to be a `#define FORWARD(x) std::forward<decltype(x)>(x)`?

My definition is


    #define FORWARD(x) static_cast<decltype(x) &&>(x)
I think, it should be [=]x.foo (x is captured by copy) or [&]x.foo
(x is captured by reference).

What about [x].foo or [&x].foo? :) Just food for thought.

We may get parsing issues then. For example,

    (Type())[x].foo

may be a valid expression under C++98/03/11 rules. If we assume that
[x].foo could be a primary-expression, then we'll get new possible
interpretation: it is (semantically incorrect) cast-expression, where
[x].foo is converted to type Type(), and such ill-formed
interpretation shall be chosen according to 8.2[dcl.ambig.res]/2.

Nikolay Ivchenkov

unread,
Apr 24, 2013, 1:56:24 PM4/24/13
to std-pr...@isocpp.org
On Wednesday, April 24, 2013 8:05:43 PM UTC+4, Giovanni Piero Deretta wrote:

The uniformity is not just for its own sake. It is vital for generic code. Think for example about std::begin/end which exist to bridge the gap between containers having member functions and other ranges which do not have them.

IMO, the analogy with std::begin/end is wrong. There is nothing bad in overloading when it allows to perform semantically equivalent/similar operations uniformly (as std::begin/std::end do). INVOKE is a sort of dirty overloading, where semantically different operations are combined in a single notation. No sane generic code should rely on such kind of uniformity. We already have list-initialization, which often dramatically reduces readability (in some cases even language lawyers can't figure out what's going on). I don't want to get another such thing - I prefer to read code, rather than to decrypt it.

Xeo

unread,
Apr 24, 2013, 3:27:26 PM4/24/13
to std-pr...@isocpp.org
Thanks for forwarding my mail! :)


On Wednesday, April 24, 2013 7:22:53 PM UTC+2, Nikolay Ivchenkov wrote:

I want to say that an operator invocation (as well as an operator
function invocation of any form) can be wrapped in a polymorphic
lambda with explicitly provided parameters. The resulting expression
would be far from being terse, but we can get desirable functionality;
therefore, I don't see a problem in this part:

Ah, but I meant when wrapped in a single macro.
 
(Also, is FORWARD supposed to be a `#define FORWARD(x) std::forward<decltype(x)>(x)`?

My definition is

    #define FORWARD(x) static_cast<decltype(x) &&>(x)

/sadpanda :(
 
We may get parsing issues then. For example,

    (Type())[x].foo

may be a valid expression under C++98/03/11 rules. If we assume that
[x].foo could be a primary-expression, then we'll get new possible
interpretation: it is (semantically incorrect) cast-expression, where
[x].foo is converted to type Type(), and such ill-formed
interpretation shall be chosen according to 8.2[dcl.ambig.res]/2.
 
Okay, that is interesting. I'll definitly add `[=]x.foo` and `[&]x.foo` as considerations.
Reply all
Reply to author
Forward
0 new messages