JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.41k stars 5.45k forks source link

Julia accepts invalid function definition with keyword arguments #49375

Open hyrodium opened 1 year ago

hyrodium commented 1 year ago

MWE:

julia> b = 3
3

julia> f(;a=b=4) = (a,b)  # This definition should throw an error
f (generic function with 1 method)

julia> f()  # Refers global variable `b`
(4, 3)

julia> f(a=5)  # Accept keyword agument `a`
(5, 3)

julia> f(b=6)  # Does not accept keyword agument `b`
ERROR: MethodError: no method matching f(; b::Int64)

Closest candidates are:
  f(; b, a)
   @ Main REPL[2]:1

Stacktrace:
 [1] kwerr(kw::NamedTuple{(:b,), Tuple{Int64}}, args::Function)
   @ Base ./error.jl:165
 [2] top-level scope
   @ REPL[5]:1

julia> methods(f)  # But this shows `b` as a keyword argument.
# 1 method for generic function "f" from Main:
 [1] f(; b, a)
     @ REPL[2]:1

julia> versioninfo()
Julia Version 1.9.0-rc2
Commit 72aec423c2a (2023-04-01 10:41 UTC)
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 16 × AMD Ryzen 7 2700X Eight-Core Processor
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-14.0.6 (ORCJIT, znver1)
  Threads: 1 on 16 virtual cores
hyrodium commented 1 year ago

I also found a related strange behavior.

julia> f(a=4)
ERROR: UndefVarError: f not defined
Stacktrace:
 [1] top-level scope
   @ REPL[1]:1

julia> a
ERROR: UndefVarError: a not defined

julia> f(a=b=4)
ERROR: UndefVarError: f not defined
Stacktrace:
 [1] top-level scope
   @ REPL[3]:1

julia> a
ERROR: UndefVarError: a not defined

julia> b  # b should not be defined.
4
fingolfin commented 1 year ago

Is this really an error? I mean, I agree it is surprising and almost guaranteed to not be what the user wants... But it still seems legit to me, at least on the surface? Here is my reasoning: In Julia everything is an expression, including the assignment b=4. So in your function definition, you are really settings a = (b=4). So you define a single kwarg named a, and the expression used to initialize it just happens to be an assignment. But it could be anything else with a side effect, too:

julia> f(;a=begin println("using default value for a"); 1 end) = a
f (generic function with 1 method)

julia> f()
using default value for a
1

julia> f(a=2)
2

This also explains your second example: f(a=b=4) first evaluates the expression b=4, which as a side effect sets b; the evaluated value is 4 which is then passed to f, which however does not exist, resulting in that error.

hyrodium commented 1 year ago

Thank you for the detailed explanation. I believe the return value of methods(f) in the first comment must be a bug, but I agree that the behavior in the second comment is expected.

fingolfin commented 1 year ago

This bit you mean:

julia> methods(f)  # But this shows `b` as a keyword argument.
# 1 method for generic function "f" from Main:
 [1] f(; b, a)
     @ REPL[2]:1

Yes, I agree, that seems wrong!

adienes commented 1 year ago

agree that the behavior in https://github.com/JuliaLang/julia/issues/49375#issuecomment-1510524436 is expected.

well, it is not clear to me when this is evaluated in local scope vs global. it seems different based on whether you use the default argument vs explicitly pass? surely this is not entirely expected. it would make a lot more sense to me if the variable scope of expressions & assignments in the kwargs followed all the same rules as within the body of the function

julia> function f(a = (b = 1)); b end
f (generic function with 2 methods)

julia> f()
ERROR: UndefVarError: `b` not defined
Stacktrace:
 [1] f
   @ ./REPL[1]:1 [inlined]
 [2] f()
   @ Main ./REPL[1]:1
 [3] top-level scope
   @ REPL[2]:1

julia> f(a = (b = 2))
ERROR: MethodError: no method matching f(; a::Int64)

Closest candidates are:
  f() got unsupported keyword argument "a"
   @ Main REPL[1]:1
  f(::Any) got unsupported keyword argument "a"
   @ Main REPL[1]:1

Stacktrace:
 [1] top-level scope
   @ REPL[3]:1

julia> f()
2

that is, I might expect function foo(;a = b = c = 1) to mean the same thing as function foo(; c = 1, b = c, a = b)

vtjnash commented 1 year ago

It sounds like the scope of b (and others here) is getting multiple different results (sometimes global, sometimes local, sometimes keyword), so that seems like a bug, that can either be disallowed or converted to mean something more consistent.

adienes commented 1 year ago
julia> methods(f)  # But this shows `b` as a keyword argument.
# 1 method for generic function "f" from Main:
 [1] f(; b, a)
     @ REPL[2]:1

would this portion be closed by https://github.com/JuliaLang/julia/pull/44434 ?

vtjnash commented 1 year ago

Unlikely

matthias314 commented 1 year ago

It sounds like the scope of b (and others here) is getting multiple different results (sometimes global, sometimes local, sometimes keyword), so that seems like a bug

Except for the incorrect result of methods(f), it seems to me that the examples given so far are consistent if one says that the code computing default values has hard local scope (as does the function body). What surprises me is that the scope appears to be shared among all keywords (but not the function body):

julia> f(; a = b = 1, c = b) = c
julia> f()
1

This is actually the same for positional arguments:

julia> g(a = b = 1, c = b) = c
julia> g()
1

There is one difference, however: For f one can rename the keyword argument c to b, but for g this is not possible:

julia> f2(; a = b = 1, b = b) = b
julia> f2()
1
julia> g2(a = b = 1, b = b) = b
julia> g2()
ERROR: UndefVarError: `b` not defined

Also, the method displayed for f2 is quite strange:

julia> methods(f2)
# 1 method for generic function "f2" from Main:
 [1] f2(; ba, b)