Tips on how to use let blocks to shadow local variables and avoid accidentally modifying the original variable

I recently adapted my code using new scopes and shadowing to avoid thinking of new variable names. The simplified code is something like the following example. I originally used begin blocks instead of let blocks, so the result is completely wrong and it took me a while before I realized the problem after I went debugging some other parts of my code. To shadow the original variables, I have to add local to all variables defined in the let blocks, which is acceptable. But accidentally forgetting to add local may still happen, which may lead to annoying bugs. Is there any elegant way to do this?

function foo()
    x=1 # sadly const x=1 does not work
    y=2
    a = let
        local x=2 # has to put local in front of every variable
        x+y
    end 
    b = let
        y=3 # oops, forgot to add local
        x+y
    end
    (a, b)
end

Hi,
You must put the declaration into the same line as let to get the desired behaviour. From the docs of let:

Additionally, the syntax has a special meaning for comma-separated assignments
  and variable names that may optionally appear on the same line as the let:

  let var1 = value1, var2, var3 = value3
      code
  end

  The variables introduced on this line are local to the let block and the assignments are evaluated in order, with each right-hand side evaluated in the scope without considering the name on the left-hand
  side. Therefore it makes sense to write something like let x = x, since the two x variables are distinct with the left-hand side locally shadowing the x from the outer scope. This can even be a useful idiom
  as new local variables are freshly created each time local scopes are entered, but this is only observable in the case of variables that outlive their scope via closures.
5 Likes

Yes, I’m aware that you can put all the local variables after the let keyword. I also think it’s more readable this way. My concern is that the variables in the outer scope might still be modified if one’s not careful. I wish immutable local variables are a thing.

What you describe is the exact reason why I personally think that the let construct in Julia is flawed. As @laborg describes, the (first) declaration must appear on the same line as the let keyword to make the declaration local to the let block. But the assigned values are usually longer than to fit several of them on a single line, otherwise one wouldn’t need to use a let block. I have several workarounds which I use variably:

let x = <expression-to-compute-x>, # note the comma
    y = <expression-to-compute-y>  # note the missing comma
    # intentional blank line to separate the body from the declarations
    <expression-using-x-and-y>
end

If there is something before the let keyword, e.g., a variable assignment, that tends to mess up the indentation, and x and y will no longer be aligned. The trick one can use in this case is:

gobal_value = let _ = 0, # dummy assignment to "carry" the comma and the line break
    x = <expression-to-compute-x>, # note the comma
    y = <expression-to-compute-y>  # note the missing comma
    # intentional blank line to separate the body from the declarations
    <expression-using-x-and-y>
end

But has more boilerplate and more error prone than I like, especially because of its sensitivity to the placement of commas. Here’s is what I usually write instead:

global_value = begin
    local x = <expression-for-x> # no comma necessary
    local y = <expression-for-y> # local keyword makes declarations explicit
    <expression-with-x-and-y> # no blank line needed, lack of `local` is obvious
end

Or you can even use function syntax:

global_value = (function(; x, y)
    <expression-with-x-and-y>
end)(
    x = <expression-for-x>,
    y = <expression-for-y>
)

The major drawback of this form is that it is non-idiomatic for Julia, so other developers might have some difficulty recognizing what is going on.

1 Like

What is wrong with just using

gobal_value = let x, y
    x = <expression-to-compute-x>
    y = <expression-to-compute-y>
    <expression-using-x-and-y>
end
1 Like

You should use a function if you care about not modifying the outer scope.

1 Like

Hah! Strangely, I’ve never thought of that. The more I look at it, the more I agree that this is the right way to do it. I wonder why it’s not this form that the manual describes. In fact, it’s worse than that: the manual doesn’t even hint that the assignment part is optional, so it simply never occurred to me. Perhaps because of my functional programming background (SML and Haskell).

I’ll definitely add this form to my repertoire, thanks!

It is in the docs, but just a bit “hidden”, e.g. var2 is not assigned initially

  let var1 = value1, var2, var3 = value3
      code
  end
1 Like

You are right, I stand corrected. Mind you, I read this part at least a dozen times, and never ever noticed that nuance. I still think that it should be mentioned explicitly in the description as well.

That’s what I’m doing right now. Notice that if the expression to calculate the new value depends on the old value, you have to put the assignment statement after let, or assign the old value to the new variable first.

gobal_value = let x=x, y=y
    x = <expression-that-depends-on-x>
    y = <expression-that-depends-on-y>
    <expression-using-x-and-y>
end