This example is in fact almost exactly demonstrated in this comment on issue 9498, only the keyword argument has a default value. (The issue’s first example differs because the positional annotations can be different, so the methods shouldn’t override each other. methods(f)
does show them as separate methods, and it’s more intuitive that a keyworded call can’t use a method with no keywords even if the positional annotation is more specific).
Redoing the demo with methods(f), keyword method first
julia> function f(a; b) a + b end;
julia> methods(f)
# 1 method for generic function "f":
[1] f(a; b) in Main at REPL[1]:1
julia> function f(a) a + a end;
julia> methods(f)
# 1 method for generic function "f":
[1] f(a; b) in Main at REPL[3]:1
julia> f(2), f(2; b=1)
(4, 3)
Demo but keyword method second in a different session
julia> function f(a) a + a end;
julia> methods(f)
# 1 method for generic function "f":
[1] f(a) in Main at REPL[1]:1
julia> function f(a; b) a + b end;
julia> methods(f)
# 1 method for generic function "f":
[1] f(a; b) in Main at REPL[3]:1
julia> f(2; b=1)
3
julia> f(2)
ERROR: UndefKeywordError: keyword argument b not assigned
If you’ve noticed, it certainly seems as if the keyworded method actually had 2 methods, 1 throwing the error with no keywords. This would make a little more sense; in the first case, the error-throwing method was overwritten with a proper implementation, and vice versa in the second case. But where is it?
Let’s try just the keyworded method with a default value this time so we don’t throw errors. I will use ...
to omit some printed lines for brevity.
julia> function f(a; b=0) a+b end;
julia> methods(f)
# 1 method for generic function "f":
[1] f(a; b) in Main at REPL[14]:1
julia> @code_warntype f(1)
MethodInstance for f(::Int64)
from f(a; b) in Main at REPL[14]:1
...
1 ─ %1 = Main.:(var"#f#3")(0, #self#, a)::Int64
└── return %1
julia> @code_warntype f(1; b=10)
MethodInstance for (::var"#f##kw")(::NamedTuple{(:b,), Tuple{Int64}}, ::typeof(f), ::Int64)
from (::var"#f##kw")(::Any, ::typeof(f), a) in Main at REPL[14]:1
...
5 ┄ %16 = Main.:(var"#f#3")(b, @_3, a)::Int64
└── return %16
The keywordless call directly lists the 1 f(a; b)
method, but the keyworded call does not. It lists a var"#f##kw"
functor type. Both calls have the callee var"#f#3
, only the keywordless call has the default value.
julia> methods(var"#f#3")
# 1 method for generic function "#f#3":
[1] var"#f#3"(b, ::typeof(f), a) in Main at REPL[14]:1
julia> var"#f#3"(10, f, 1)
11
julia> methods(var"#f##kw") # huh, constructor was omitted
# 0 methods for type constructor
julia> Base.issingletontype(var"#f##kw") # singleton.instance loophole!
true
julia> methods(var"#f##kw".instance)
# 1 method for anonymous function "f##kw":
[1] (::var"#f##kw")(::Any, ::typeof(f), a) in Main at REPL[14]:1
julia> var"#f##kw".instance((b=10,), f, 1)
11
So there you have it, keyworded calls are handled by a hidden singleton functor instance, and the keywordless calls are directly handled by the declared keyword method. The latter clashes with position-only methods, independently of the former. Both forward to a callee, which is what you want to @code_warntype
to actually probe type instabilities (or use Cthulhu.jl to descend through callees).