On Thursday, 25 May 2017 at 03:10:04 UTC, Adam D. Ruppe wrote:
Snip

I think the discussion is going in two orthogonal directions, which are:

1) Capture by value. This is the ability to have a lambda which behaves as expected in the example Vittorio showed

  void delegate()[] arr;

   foreach(i; 0..5)
   {
       arr ~= () => writeln(i);
   }

   foreach(f; arr)
   {
       f();
   }

This is going to print "4 4 4 4"

2) Allocation of the lambda context. Whether this is performed by the GC, in the stack, or with another mechanism.

The reason these two points are orthogonal is that, as Adam showed, the ability to achieve one of the point is independent from the other (the capture by reference context allocation can be changed with the scope keyword, and from my understanding it's an implementation detail, it does not guarantee the use of the GC. And the value of capture by value could be allocated on the heap with the GC).


I see two weaknesses in this proposal:

1) The lambda is not compatible with a delegate anymore

2) There need to be a special notation for reference capture

Adam said
I'm against that, no need adding it and it complicates the whole thing. For example, "ref int _i;" as a struct member; there's no such thing in D. (the compiler could do it but still). And you'd have to explain the lifetime. Just no point doing this, the current behavior is completely fine for this.



The other problem I see, which is easily noticeable when defining the new grammar, is that we are proposing a grammar which is a superset of the current one, but we are proposing it as an addition.


One of the assumption of D is that the GC is not a problem in the vast majority of the situations, and you want things to just work. I think there is big value in the capture context not having to be explicit (even if usually I much prefer explicity over implicity).


Considered this, I'd propose an extension of the lambda syntax instead of an addition of a ValueFunctionLitteral.

Given the syntax
[...] (...) ... {...}
which is composed of what I will call "capture", "parameters", "attributes", "body" (with capture and attributes optional)
the semantic would be:

- if no capture is provided the behavior is equivalent to today (automatic capturing by reference in a context allocated however the compiler prefers). This provides full backward compatibility.
Example
(int i) { writeln(i + j);}

j is captured by reference in a context allocated (possibly) on the GC. This is compatible with a delegate void(int).


- if capture is provided, then the lambda is rewritten as a struct, which the explicit variables in the capture list being captured by value, and the remaining being captured by reference (so there would still be implicit capture, which is considered convenient in many programming languages). This is potentially unsafe since a reference could be copied when copying the struct, and the struct might outlive the referred object.
Example
[x] (int i) { writeln(i + j + x);}

x is captured by value in a local struct.
j is captured by reference in a local struct.
This is not a delegate anymore (different ABI).


- if "delegate" is used, then the capture context is allocated however the compile prefers

[x] delegate (int i) { writeln(i + j + x);}

x is captured by value in a (possibly) GC context.
j is captured by reference in a (possibly) GC context.
This is a delegate (compatible ABI).


This thus is an extension of the lambda syntax to allow capturing by value, but maintaining all the current properties and providing a way to remain compatible with the delegate functions. You can also use the @nogc attribute to verify that the GC is not used (and in case also "delegate" is used "scope" will need to be used so that the compiler does not require the GC).

What are your opinions?
Please state if there are some misconceptions on the current situation.

Reply via email to