10

Shouldn't Go compiler capture for...range loop variables as a locally assigned closure variable?

Long Version:

This caused me some confusion in C# too and I was trying to understand it; that why it is fixed in C# 5.0 foreach (reason: the loop variable can not change inside the body of loop) and the reasoning for not fixing it in C# for loops (reason: the loop variable can change inside the body of loop).

Now (to me) for...range loops in Go seems pretty much like foreach loops in C#, but despite the fact that we can not alter those variables (like k and v in for k, v := range m { ... }); still we have to copy them to some local closures first, for them to behave as expected.

What is the reasoning behind this? (I suspect it's because Go treats any for loop the same way; but I'm not sure).

Here is some code to examine described behavior:

func main() {
    lab1() // captured closure is not what is expected
    fmt.Println(" ")

    lab2() // captured closure is not what is expected
    fmt.Println(" ")

    lab3() // captured closure behaves ok
    fmt.Println(" ")
}

func lab3() {
    m := make(map[int32]int32)
    var i int32
    for i = 1; i <= 10; i++ {
        m[i] = i
    }

    l := [](func() (int32, int32)){}
    for k, v := range m {
        kLocal, vLocal := k, v // (C) captures just the right values assigned to k and v
        l = append(l, func() (int32, int32) {
            return kLocal, vLocal
        })
    }

    for _, x := range l {
        k, v := x()
        fmt.Println(k, v)
    }
}

func lab2() {
    m := make(map[int32]int32)
    var i int32
    for i = 1; i <= 10; i++ {
        m[i] = i
    }

    l := [](func() (int32, int32)){}
    for k, v := range m {
        l = append(l, func() (int32, int32) {
            kLocal, vLocal := k, v // (B) captures just the last values assigned to k and v from the range
            return kLocal, vLocal
        })
    }

    for _, x := range l {
        k, v := x()
        fmt.Println(k, v)
    }
}

func lab1() {
    m := make(map[int32]int32)
    var i int32
    for i = 1; i <= 10; i++ {
        m[i] = i
    }

    l := [](func() (int32, int32)){}
    for k, v := range m {
        l = append(l, func() (int32, int32) { return k, v }) // (A) captures just the last values assigned to k and v from the range
    }

    for _, x := range l {
        k, v := x()
        fmt.Println(k, v)
    }
}

As it is shown in lab1, at the comment // (A) we get just the last values from the range; the output is like printing 9,9 ten times instead of showing expected result like 1,1, 2,2, ... (and of-course maps are not necessarily sorted in Go so we may see 3,3 ten times as the last pair of values; instead of 10,10 ten times as the last pair of values). The same goes for code at comment // (B) at lab2, which was expected because we are trying to capture outer variables inside the inner scope (I put this one too just to try that). In lab3 at code at comment // (C) everything works fine and you will see ten pairs of numbers there like 1,1, 2,2, ....

I was trying to use closure+function as a replacement for tuples in Go.

Community
  • 1
  • 1
Kaveh Shahbazian
  • 12,391
  • 12
  • 75
  • 137
  • 1
    Could you make your examples shorter and clearer? Maybe it's the fact that it's the middle of the night here but I can't see what you're expecting and what's happening from your examples. – Not_a_Golfer Nov 01 '14 at 21:14
  • I've described the code and the purpose I've wrote it this way and encountered this. – Kaveh Shahbazian Nov 01 '14 at 21:38
  • 1
    This is a FAQ: https://golang.org/doc/faq#closures_and_goroutines – dyoo Nov 01 '14 at 23:07
  • @dyoo Thanks; but that's exactly what does not feels right to me about `for...range` loops. – Kaveh Shahbazian Nov 01 '14 at 23:13
  • Besides @dyoo's Link, there is [Common Mistakes: Using goroutines on loop iterator variables](https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables) – Ivan Chau Jun 27 '15 at 07:17

1 Answers1

16

Do you want the closure over the variable or the value? For example,

package main

import "fmt"

func VariableLoop() {
    f := make([]func(), 3)
    for i := 0; i < 3; i++ {
        // closure over variable i
        f[i] = func() {
            fmt.Println(i)
        }
    }
    fmt.Println("VariableLoop")
    for _, f := range f {
        f()
    }
}

func ValueLoop() {
    f := make([]func(), 3)
    for i := 0; i < 3; i++ {
        i := i
        // closure over value of i
        f[i] = func() {
            fmt.Println(i)
        }
    }
    fmt.Println("ValueLoop")
    for _, f := range f {
        f()
    }
}

func VariableRange() {
    f := make([]func(), 3)
    for i := range f {
        // closure over variable i
        f[i] = func() {
            fmt.Println(i)
        }
    }
    fmt.Println("VariableRange")
    for _, f := range f {
        f()
    }
}

func ValueRange() {
    f := make([]func(), 3)
    for i := range f {
        i := i
        // closure over value of i
        f[i] = func() {
            fmt.Println(i)
        }
    }
    fmt.Println("ValueRange")
    for _, f := range f {
        f()
    }
}

func main() {
    VariableLoop()
    ValueLoop()
    VariableRange()
    ValueRange()
}

Output:

VariableLoop
3
3
3
ValueLoop
0
1
2
VariableRange
2
2
2
ValueRange
0
1
2

References:

The Go Programming Language Specification

Function literals

Function literals are closures: they may refer to variables defined in a surrounding function. Those variables are then shared between the surrounding function and the function literal, and they survive as long as they are accessible.

Go FAQ: What happens with closures running as goroutines?

To bind the current value of v to each closure as it is launched, one must modify the inner loop to create a new variable each iteration. One way is to pass the variable as an argument to the closure.

Even easier is just to create a new variable, using a declaration style that may seem odd but works fine in Go.

peterSO
  • 146,831
  • 29
  • 256
  • 250
  • closure over variable in `for` loops; closure over value in `for...range` loops. – Kaveh Shahbazian Nov 01 '14 at 23:15
  • 3
    An alternative to `i := i` I've seen the Go folks use is making the inner function take `i` as an argument. As to whether it should work this way, like all sorts of design questions, it's arguable, but when you buy into a language ecosystem you've got to work with what you have. – twotwotwo Nov 01 '14 at 23:29
  • 1
    @KavehShahbazian: Your choice. See my revised answer. Use `k, v := k, v` for `range` in your example. – peterSO Nov 01 '14 at 23:36
  • @twotwotwo You are absolutely right about ecosystem. But it was admittedly not a good choice in C#; and they've fixed it. I just want to know the reason (like even this is not possible because of Go compiler internals or the like) and as I've learnt from googling around it's been there for some time. – Kaveh Shahbazian Nov 02 '14 at 00:03
  • 1
    Ahh, interesting question. Maybe designers just find it easier (or cleaner?) for all loop variables to be treated as a single variable declared outside the scope for capturing purposes. Python is this way (`[lambda: i for i in range(10)][0]()` prints `9`); Perl, surprisingly, is not (`my @subs; for my $i (1..10) { push @subs, sub { print "$i"; } }; $subs[0]->()` prints `1`). – twotwotwo Nov 02 '14 at 00:14
  • @KavehShahbazian: There is no correct semantics: [Differences_in_semantics](http://en.wikipedia.org/wiki/Closure_%28computer_programming%29#Differences_in_semantics). Anyway, it's not going to change in Go 1: [Go 1 and the Future of Go Programs](http://golang.org/doc/go1compat). You can easily obtain whatever behavior you want. – peterSO Nov 02 '14 at 00:29
  • @peterSO You are right, there is no correct semantics. But there are surprising semantics in every PL that one should be aware of. – Kaveh Shahbazian Nov 02 '14 at 07:45