For helping beginners understand, I always think box-and-arrow diagrams are helpful. Each 'name introduced by let' creates a box (which either holds a value, for value types, or holds an arrow that points to a box on the heap, for reference types). A new 'let' on the same name creates a new box with the same name (shadowing), whereas <- changes the value in a named box. A la

let foo = [1]

// in diagrams, '*' signifies what just changed
foo
-----
| | // can choose to go into more
| *-+----> [1] // detail for heap display
| | // of lists, not relevant here
-----

let mutable foo = [1;2]

foo (shadowed)
-----
| |
| --+----> [1]
| |
-----
foo
-----
| |
| *-+----> [1;2]
| |
-----

foo <- []

foo (shadowed)
-----
| |
| --+----> [1]
| |
-----
foo
-----
| |
| *-+----> []
| |
-----

By on 1/13/2009 12:27 PM ()

For helping beginners understand, I always think box-and-arrow diagrams are helpful. Each 'name introduced by let' creates a box (which either holds a value, for value types, or holds an arrow that points to a box on the heap, for reference types). A new 'let' on the same name creates a new box with the same name (shadowing), whereas <- changes the value in a named box. A la

let foo = [1]

// in diagrams, '*' signifies what just changed
foo
-----
| | // can choose to go into more
| *-+----> [1] // detail for heap display
| | // of lists, not relevant here
-----

let mutable foo = [1;2]

foo (shadowed)
-----
| |
| --+----> [1]
| |
-----
foo
-----
| |
| *-+----> [1;2]
| |
-----

foo <- []

foo (shadowed)
-----
| |
| --+----> [1]
| |
-----
foo
-----
| |
| *-+----> []
| |
-----

I would have thought that the shadowed version would be overwritten or removed. Is there any way to access the shadowed version? Is there any good reason to let programmers shadow values in this way?

By on 1/13/2009 12:32 PM ()

I would have thought that the shadowed version would be overwritten or removed. Is there any way to access the shadowed version? Is there any good reason to let programmers shadow values in this way?

The "#light" syntax allows one to drop some language syntax in favor of indenting, which in this case may obscure what is actually being defined. Perhaps the symantically identical "regular syntax" version will shed some light (pun intended); note that indentation is not significant in the following:

1
2
3
4
5
let a() = 
    let foo = [1] in
        printfn "%A" foo;
        let foo = [1;2] in
            printfn "%A" foo

Each "let ... in ..." binding above in effect opens up a new scope. The second foo label shadows the first foo label. I don't know if that means [1] can now be garbage collected, but there are logically two values in memory now foo=[1] and foo=[1;2]. This differs from mutable assignment, where [1] would be replaced by [1;2].

To prove the point, run the following code. Note that b() produces different results for each foo value, showing that both are independent of each other, whereas c() produces the same result for the values, because the original memory location (the reference cell called foo) was overwritten.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
> let b() =
    let foo = [1] in
    let out_foo_1() = printfn "foo_1 = %A" foo in
    let foo = [1;2] in
    let out_foo_2() = printfn "foo_2 = %A" foo in
    do out_foo_1();
    do out_foo_2()
 
let c() =
    let foo = ref [1] in
    let out_foo_1() = printfn "foo_1 = %A" !foo in
    do foo := [1;2];
    let out_foo_2() = printfn "foo_2 = %A" !foo in
    do out_foo_1();
    do out_foo_2();;

val b : unit -> unit
val c : unit -> unit

> b();;
foo_1 = [1]
foo_2 = [1; 2]
val it : unit = ()

> c();;
foo_1 = [1; 2]
foo_2 = [1; 2]
val it : unit = ()
>

Think of "let" as declaring a constant, and you won't be far off. To get a "variable" you must say either "let mutable foo = x" or, when using the value in a closure like out_foo_x above, you must use a reference cell, in which case the let binding declares a constant reference to a cell, and the cell is the thing that can change. In fact, ref could be (is?) defined as:

1
type 'a ref = { mutable contents : 'a; }
By on 1/14/2009 9:50 PM ()

You might also find the behaviour of this code illuminating (or confusing). You can use parentheses to limit the scope of a let binding.

1
2
3
4
5
let foo = [1] in
printfn "%A" foo;
(let foo = [1;2] in
printfn "%A" foo);
printfn "%A" foo;

It prints

1
2
3
[1]
[1;2]
[1]
By on 1/14/2009 10:33 PM ()

They don't really have the same scope. A "let ... in expr" expression opens a new scope for expr. It's like if you use { } is some other languages.

By the way, it's sometimes useful to shadow values. I often write "let foo = defaultArg foo 0" when I use optional arguments.

You can't access shadowed values. You first need to close the inner scope if you want to use the outer value.

Laurent.

By on 1/13/2009 1:11 PM ()

This is the behavior in the interactive console (not sure why) but in a regular code file F# complains unless it is mutable (and you still need to use the <- syntax).

By on 1/13/2009 9:17 AM ()

Mike,

Then the beginner types...

1
2
3
4
5
let a() = 
    let foo = [1]
    printfn "%A" foo
    let foo = [1;2]
    printfn "%A" foo

...in a regular code file and asks the same question.

By on 1/13/2009 9:45 AM ()

Mike,

Then the beginner types...

1
2
3
4
5
let a() = 
    let foo = [1]
    printfn "%A" foo
    let foo = [1;2]
    printfn "%A" foo

...in a regular code file and asks the same question.

I didn't think about doing it in a function as I don't see any good reason for doing that. I thought that once it is defined in a particular scope that that is it. Interesting but again I don't see any reason as to why one would do this in the first place.

By on 1/13/2009 12:29 PM ()

And then you explain that they are in fact different foo's, and the beginner is not altering them. The second 'let' indicates this. It's not:

1
2
3
4
5
6
7
8
9
10
let a() = 

    let foo = [1]

    printfn "%A" foo

    foo = [1;2]

    printfn "%A" foo

And the same goes for F# interactive: you're not mutating, you're defining a new variable which just happens to have the same name as the previous one.

It seems to me that you should be able to explain this to a beginner. I do agree that the behavior from the code block above can be confusing at first - although I use it sometimes myself to do a long calculation in steps without having to invent new nonsensical names for all the intermediaries.

Kurt

By on 1/13/2009 11:29 AM ()
IntelliFactory Offices Copyright (c) 2011-2012 IntelliFactory. All rights reserved.
Home | Products | Consulting | Trainings | Blogs | Jobs | Contact Us | Terms of Use | Privacy Policy | Cookie Policy
Built with WebSharper