Clarification on Hard Scope within functions and function arguments

I’m sure I am missing something simple here so I apologize in advance. But I am a little confused as to why this first assignment works.

using Dates

julia> function currentmin(minute = minute(now()))
              minute
              end
currentmin (generic function with 2 methods)

julia> currentmin()
22

whereas this one returns an error

julia> function currentmin1()
              minute = minute(now())
              end
currentmin1 (generic function with 1 method)

julia> currentmin1()
           
ERROR: UndefVarError: minute not defined

According to the documentation on scope, functions introduce a Hard scope: If x is not already a local variable and assignment occurs inside of any hard scope construct (i.e. within a let block, function or macro body, comprehension, or generator), a new local named x is created in the scope of the assignment.

I thought maybe the assignment worked in currentmin because minute technically hadn’t been called until the assignment was made. But then why doesn’t it work for currentmin1?

Even if the arguments of the function are being made outside the hard scope of the function shouldn’t the assignment still fail because of the global scope of the module?

using Dates

julia> minute = 45

ERROR: cannot assign a value to variable Dates.minute from module Main

1 Like

As far as I understand you can interpret the function parameter as this:

julia> x = 1
1

julia> f = let x = x
           x += 1
       end
2

julia> x
1

# the same as:
julia> x = 1
1

julia> g(x=x) = x += 1
g (generic function with 2 methods)

julia> g(x)
2

julia> x
1

meaning that the left side of the parameter assignment defines a variable local to the scope of the function, the right side is the global (or outer scope) variable. Thus, in your first function, the left side minute is a variable local to the scope of the function, the right side minute is a reference to the global minute function.

In your second example there is an ambiguity which raises an error, because you are assigning a local variable name and trying to call a function with the same label.

4 Likes

This is a well written explanation, which I was searching for yesterday, but all I was able to create was overly complex and even not understandable by myself :slight_smile:

But I still want to add that this

and this

can be easily shown by the code itself:

julia> @code_lowered currentmin()
CodeInfo(
1 ─ %1 = Main.now()
│   %2 = Main.minute(%1)
│   %3 = (#self#)(%2)
└──      return %3
)

You see the call to Main.minute(...) versus in

julia> @code_lowered currentmin1()
CodeInfo(
1 ─ %1 = Main.now()
│   %2 = (minute)(%1)
│        minute = %2
└──      return %2
)

where only local identifiers minute exist, with the ambiguity of it.
The parser can do it right, as you probably expected it, with e.g.:

julia> function currentmin2()
          m = minute(now())
       end

julia> @code_lowered currentmin2()
CodeInfo(
1 ─ %1 = Main.now()
│   %2 = Main.minute(%1)
│        m = %2
└──      return %2
)

Still, there is the question, that the parser could do it right for currentmin1() because the first usage of identifier minute must be the function Main.minute(...), so why isn’t it happening like that?

I don’t know if it is as it is by purpose, but I think it is good, that an error is raised, because more complex functions can easily be a nightmare to debug if the parser tries to be overly smart in such cases.

2 Likes

Thanks! Really appreciate both your explanations! It took me awhile to really understand the dynamic but it really clears up a lot in terms of how the function arguments are being defined.

@oheil just to clarify I am reading this correctly when @code_lowered returns an identifier in parenthesis followed by the identifier without as in as in (minute) ,minute here

this means that there is an ambiguity which will raise an error?

The paranthesis around (minute) don’t mean anything, it’s the same as

minute(%1)

and means call to function minute(...). Don’t know why they are there.
The ambiguity is not visible here, but the parser has seen the ambiguity and has decided to put a

minute(...)

instead of

Main.minute(...)

because, and this is a guess, the parser choose to use all identifiers minute as local, as it is a local variable, despite the obvious call to a global function. This is not mandatory, as you can see in currentmin2(), but it would mean, that the parser would need to differentiate between identifiers as variables and identifiers as functions. I don’t know if it is differentiated or how parsing works in this case.
Anyways, the outcome is good in this case, as it helps to avoid using identifier minute as a variable name here.

By the way, the Dates API is more to using minute() as Dates.minute(). This is the way as it is used throughout the documents. The best way is sticking to the docs, like:

julia> function currentmin1()
            minute = Dates.minute(now())
       end

julia> @code_lowered currentmin1()
CodeInfo(
1 ─ %1 = Dates.minute
│   %2 = Main.now()
│   %3 = (%1)(%2)
│        minute = %3
└──      return %3
)
2 Likes