September 29, 2021

On Wednesday, 29 September 2021 at 22:04:33 UTC, jfondren wrote:

>

On Wednesday, 29 September 2021 at 16:47:23 UTC, Steven Schveighoffer wrote:

>

[...]

or,

[...]

Lol 👍

September 30, 2021

On Wednesday, 29 September 2021 at 16:47:23 UTC, Steven Schveighoffer wrote:

>

What we need is a syntax to specify which values are captured and which variables are referenced.

What I normally do if I need something like this is:

foreach(int i; [1, 2, 3]) {
   dgList ~= (b) { return {writeln(b);};} (i + 2);
   // or less error prone:
   dgList ~= (i) { return {writeln(i + 2);};} (i);
}

This is where things go off rails. We don't need any new syntax. We need to stop adding a new gizmo every time something is not doing the right thing. The ed result is that the original thing still don't do the right thing and the gizmo also doesn't do the right thing because it has been though to solve a specific edge case.

Now, foreach is a high level construct, and just get the same semantic as what it lowers into. Let's use while loops.

int i = 1;
while (i < 4) {
   dgList ~= { writeln(i + 2); }; // still outputs 5 5 5
}

Now, this is expected isn't it? There is one and only one i variable. But let's change things a bit.

int i = 1;
while (i < 4) {
   int n = i + 2;
   dgList ~= { writeln(n); }; // still outputs 5 5 5
}

Now this is wrong. A new n variable is created at each loop iteration, this isn't the same n. It's easy to convince oneself that this is the case: n can be made immutable and the code still compiles, which is evidence that the semantic is that each loop iteration has a new n variable.

So either we create a new closure for each loop iteration (if a variable locale to the loop is captured), or making n immutable must be rejected, because either n in one variable across all iterations, and it is mutated, or it is not, but it can't be both.

For completeness, it must be noted that the former tends to b what's expected, this is what C# is doing for instance.

September 30, 2021

On Wednesday, 29 September 2021 at 16:23:34 UTC, Imperatorn wrote:

>

I'm still a bit confused. What is the recommended approach here? Should we fix the language or have best practices/documentation on how to "work around" the problem.

I'm afraid this topic has come again and again, and Walter consistently says that it is not a bug, so we are stuck complaining. Maybe you can convince him.

September 30, 2021

On Thursday, 30 September 2021 at 01:00:40 UTC, deadalnix wrote:

>

On Wednesday, 29 September 2021 at 16:23:34 UTC, Imperatorn wrote:

>

I'm still a bit confused. What is the recommended approach here? Should we fix the language or have best practices/documentation on how to "work around" the problem.

I'm afraid this topic has come again and again, and Walter consistently says that it is not a bug, so we are stuck complaining. Maybe you can convince him.

He doesn't exactly that it's not a bug, but that some design that leads to it is desirable, and that some solutions are undesirable. Specifically this part:

>

D closures are "by reference" rather than "by value". I make use of it being by reference all the time, as well as it being much more efficient. Changing it would likely break all sorts of code.

But the bugzilla issue isn't closed, he added the 'safe' keyword as it breaks immutability, and there's all kinds of language that accepts that this is a bug (edited to add emphasis):

>

Here's a better illustration of the problem: ...

>

The most practical solution is ...

Very nice explanation with the while loops, btw.

September 30, 2021

On Thursday, 30 September 2021 at 00:59:26 UTC, deadalnix wrote:

>

[...]
Now, this is expected isn't it? There is one and only one i variable. But let's change things a bit.

int i = 1;
while (i < 4) {
   int n = i + 2;
   dgList ~= { writeln(n); }; // still outputs 5 5 5
}

Now this is wrong.

Why is this wrong ? Do you expect one alloca per iteration for n ?
It's pretty obvious that variables declared in loops also use the same alloca, always. Otherwise what would be required is stack save and restore after each iteration.

September 30, 2021

On Thursday, 30 September 2021 at 02:03:39 UTC, Basile B. wrote:

>

On Thursday, 30 September 2021 at 00:59:26 UTC, deadalnix wrote:

>

[...]
Now, this is expected isn't it? There is one and only one i variable. But let's change things a bit.

int i = 1;
while (i < 4) {
   int n = i + 2;
   dgList ~= { writeln(n); }; // still outputs 5 5 5
}

Now this is wrong.

Why is this wrong ? Do you expect one alloca per iteration for n ?

No, what I expect is for the closure to close over the n, and to do whatever it needs to do for that to work. In basically every language that exists the expected behavior from this code is 3 4 5. Doing it otherwise is like PHP getting the ternary operator backwards -- it's not different because it's innovating, it's different because it's wrong.

September 30, 2021

On Thursday, 30 September 2021 at 02:25:53 UTC, jfondren wrote:

>

In basically every language that exists the expected behavior from this code is 3 4 5.

Exceptions: Python, Nim, Rust, probably C++, probably Zig.

In order:

funcs = []

for i in range(0, 3):
    n = i + 2
    funcs.append(lambda: print(n))

for f in funcs:
    f()  # output: 4 4 4
var funcs: seq[proc(): void]

for i in 0..2:
  let n = i + 2
  funcs.add (proc = echo n)

for f in funcs:
  f()  # output: 4 4 4
fn main() {
    let mut funcs = Vec::new();
    for i in 0 .. 3 {
        let n = i + 2;
        funcs.push(|| println!("{}", n));
//                 --                ^ borrowed value does not live long enough
//                 |
//                 value captured here
    }
//  - `n` dropped here while still borrowed
    for f in funcs {
        f();
    }
}

So, more like, "basically every GC language that exists", except for Python which has famously garbage lambdas. At the least, you can't say that this behavior is a barrier to popularity with Python getting lambdas so wrong.

The workaround in Rust's case is to add a move before the || there.

Nim documents this exact complaint in https://nim-lang.org/docs/manual.html#closures-creating-closures-in-loops , which points to stdlib workarounds.

September 30, 2021

On Thursday, 30 September 2021 at 01:19:54 UTC, jfondren wrote:

>

He doesn't exactly that it's not a bug, but that some design that leads to it is desirable, and that some solutions are undesirable. Specifically this part:

>

D closures are "by reference" rather than "by value". I make use of it being by reference all the time, as well as it being much more efficient. Changing it would likely break all sorts of code.

I think that D could do the captures by ref, always:

If the lambda lives longer than the parent scope of one of its capture then the capture must be copied to a new'd value, (as updating the original makes no sense btw) and otherwise it can be a true reference to the original thing.

September 30, 2021

On Thursday, 30 September 2021 at 00:59:26 UTC, deadalnix wrote:

>

On Wednesday, 29 September 2021 at 16:47:23 UTC, Steven Schveighoffer wrote:

>

[...]

This is where things go off rails. We don't need any new syntax. We need to stop adding a new gizmo every time something is not doing the right thing. The ed result is that the original thing still don't do the right thing and the gizmo also doesn't do the right thing because it has been though to solve a specific edge case.

[...]

Yes. Declaring a new variable inside the loop should make the closure capture that unique variable.

If D does not do that, it's wrong.

Fixing that should not break code.

C# fixed this and the result was eternal happiness.

September 30, 2021

On Thursday, 30 September 2021 at 02:25:53 UTC, jfondren wrote:

>

On Thursday, 30 September 2021 at 02:03:39 UTC, Basile B. wrote:

>

On Thursday, 30 September 2021 at 00:59:26 UTC, deadalnix wrote:

>

[...]
Now, this is expected isn't it? There is one and only one i variable. But let's change things a bit.

int i = 1;
while (i < 4) {
   int n = i + 2;
   dgList ~= { writeln(n); }; // still outputs 5 5 5
}

Now this is wrong.

Why is this wrong ? Do you expect one alloca per iteration for n ?

No, what I expect is for the closure to close over the n, and to do whatever it needs to do for that to work. In basically every language that exists the expected behavior from this code is 3 4 5. Doing it otherwise is like PHP getting the ternary operator backwards -- it's not different because it's innovating, it's different because it's wrong.

It is worse than this.

PHP getting the ternary operator backward is unfortunate, error prone and all, but all in all, it is consistent.

This is inconsistent, which is much more serious problem, as it means the language is unable to provide the invariants its constructs are supposed to provide.