Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

It's not crazy. It's just the difference between a pointer and a value, which is like comp sci 101.

I think the main things that make it such a trap is that the variable type definition is implicit so the fact that it's a pointer becomes a bit hidden, and that easy concurrency means the value is evaluated outside of the loop execution more often.



> It's just the difference between a pointer and a value, which is like comp sci 101.

That might be the case, but my comp sci 101 was 15 odd years ago now and since then I have _never_ had to think about pointers vs values, until I started a Go project a few years ago. But even that was more comprehensible than the pointer wizardry we had to do in C/C++ back when.

I don't want to have to think about managing my application's memory, I much prefer being in the code, thinking of variable scope and maintainability which in a lot of languages automatically translates to healthy memory usage.


>It's not crazy.

No. Full disagree.

Array represents a concept of holding multiple values (let's simplify) of the same type.

Loop (not index based) over array represents concept of going *over* array's elements and executing some code body for each of it.

Now, if the behaviour isn't that loop's body is executed for each array element (let's forget about returns, breaks, etc)

then the design is terrible (or implementation, but that'd mean that it was a bug)

I have totally no idea how can you design this thing in such a unintuitive way unless by mistake/accidentally.


I don't know much about Go but the design seems very intuitive to me. You're doing something like (let ((i variable)) (loop (setf i ...) ...body)), which if there is a closure in the loop body will capture the variable i and also subsequent mutations.

The fix is to make a new variable for each iteration, which is less obvious implementation wise but as per the post works better if you're enclosing over the loop variable.


While I agree that the design is very clear for the cases illustrated here, and I am a bit puzzled on why the Go designers chose to change these as well, the design is not at all clear for the other case:

  for i, v := range []int{1, 2, 3} {
    funcs = append(funcs, func() {fmt.Printf("%v:%v, ", i, v)})
  }
  for _, fun := range funcs {
    fun()
  } //prints 2:3, 2:3, 2:3
The reason why this happens is clear. But, it's not what people expect from the syntax, not at all. And it's also a behavior that is never useful. There is 0 reason to capture the loop variables, as evidenced by the fact that none of the languages that have started like this and taken a breaking change to switch to the expected behavior has found even a single bug caused by this breaking change.


> And it's also a behavior that is never useful.

False. There are cases where it is useful to have the loop variable available directly. For example, you can add one to the loop variable to skip an iteration, which would not work with an iteration-local loop variable.


In a for-in-range loop, the variables are read-only inside the loop body, so there is no way to skip this.

I do agree that there are reasons to modify the iteration variable in a C-style for loop, so I am surprised that those loops are being modified as well. C#, which went through a similar change, did NOT apply such a change for those for loops.


And an optimizing compiler can reduce your latter case to the former, if they can prove it's safe to do so.


The loop semantics do not have anything to do with arrays. The point of confusion is whether a new slot for data is being created before each iteration, or whether the same slot is being used for each iteration. It turns out that the same slot is being used. The Go code itself is clear `for i := 0; i < 10; i++`. `i := 0` is where you declare i. Nothing else would imply that space is being allocated for each iteration; the first clause of the (;;) statement is only run before the loop. So you're using the same i for every iteration. This is surprising despite how explicit it is; programmers expect that a new slot is being allocated to store each value of i, so they take the address to it, and are surprised when it's at the same address every iteration. (Sugar like `for i, x := range xs` is even more confusing. The := strongly implies creating a new i and x, but it's not doing that!)

Basically, here are two pseudocode implementations. This is what currently happens:

     i = malloc(sizeof(int))
     *i = 0
   loop:
     <code>
     *i = *i + 1
     goto loop if *i < 10
This is what people expect:

     secret = malloc(sizeof(int))
     *secret = 0
   loop:
     i = malloc(sizeof(int))
     *i = *secret
     <code>
     *secret = *secret + 1
     goto loop if *secret < 10
You can see that they are not crazy for picking the first implementation; it's less instructions and less code, AND the for loop is pretty much exactly implementing what you're typing in. It's just so easy to forget what you're actually saying that most languages are choosing to do something like the second example (though no doubt, not allocating 8 bytes of memory for each iteration).

Remember, simple cases work:

    for i := 0; i < 10; i++ {
        fmt.Println(i) // 0 1 2 3 4 ...
    }
It's the tricky cases that are tricky:

    var is []*int
    for i := 0; i < 10; i++ {
        is = append(is, &i)
    }
    for _, i := range is {
       fmt.Println(*i) // 9 9 9 9 9 ...
    }
If you really think about it, the second example is exactly what you're asking for. You declared i into existence once. Of course its address isn't going to change every iteration.


>The loop semantics do not have anything to do with arrays.

Loop in general or "for each" style loop, that's huge difference.

The 2nd one has a lot to do with collections.

>You can see that they are not crazy for picking the first implementation; it's less instructions and less code

Yes, it is not crazy when you're looking at it from the reverse engineering / implementation side

but if you start thinking about it from user's perspective then it is very bad behaviour

because they used "foreach" like loop which is a concept of walking thru every element of collection.


I still don't see how looping over a collection is different from looping over a sequence of numbers from 1 to n.


Depends what do you actually mean by sequence, but mostly purpose.

Normal "for" is like: repeat this code body as long as condition is satisfied

Foreach is more like: walk thru this collection

Look (c#):

foreach (var item in items) ...

for (int i=0; i<10; i++) { }

In the 2nd version it is possible to jumps ahead, back, do not move, etc. Generally play around "i's" values

Meanwhile I haven't seen yet any1 trying to do anything like this in foreach, because it is meant for just walking thru collection


I'm getting 10s for your last code example (not 9s)


Ah yup. I didn't test it and you normally never see i after that last increment!


If I understood the example, Java had this same problem. I'm wondering if C# does as well.


It would be hard to trigger it in Java. All references are pass-by-value, so you would have to do something like creating an array, passing that array and then replacing an element in it in on every loop iteration. Unless I got something wrong, it would be hard to do this by mistake IMO.


If you do an asynchronous callback in an inner loop that tries to log the loop counter and a calculated value at the same time, you will find that the loop counter has incremented underneath you and you'll get for instance '20' for all of the logs. That was my introduction to this sort of problem.

The solution as I said elsewhere is to pop out the inner block to a separate function, where the value of the counter is captured when the outer function is called, not when the inner one runs.


I don't think you remember well how you triggered this error, since Java just doesn't allow you to reference a non-final variable from an inner function. It sounds like you're talking about code like this, but this just doesn't compile:

  for (int i = 0; i < n; i++) {
    callbacks.add(new Callback(){
      public void Call() {
        System.out.println(i); //compiler error: local variables referenced from an inner class must be final or effectively final
      }
    });
  }
  for (var f : callbacks) {
    f.Call();
  }
Note that code like this works, and does the expected thing:

  for (int i : new int[]{0, 1, 2}) {
    callbacks.add(new Callback(){
      public void Call() {
        System.out.println(i);
      }
    });
  }
  for (var f : callbacks) {
    f.Call();
  } //prints 0 1 2




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: