Named parameters with type overloads

Thanks. These approaches look interesting. I’ll see if they work as a substitute.

The approach in my example was simple in the sense that it selected the method based on the highest degree of match or specificity. I can see how that may be difficult to implement, but it seems simple conceptually.

I may have misunderstood the discussion. My interpretation of the quote below is that dispatching on keyword arguments (or named parameters if that is the more appropriate term) is complicated by the presence of arbitrary optional keyword arguments which I interpreted to be kwargs... in my example. One complication is that kwargs... can introduce anything into the function.

Sorry for digging this up but I was recently frustrated by Julia’s code readability. Please consider the following solution:

  • Keyword argument design stays as it is now. (I think there is a case for having arguments not taking part in dispatch).
  • When declaring a method names of positional arguments become part of the method signature and participate in dispatch.
  • When calling a method there is an option to supply some or all positional arguments by names.
  • When calling a method if names of positional arguments are omitted everything acts as before.
  • When calling a method if names of positional arguments are provided both argument types and names should match a signature.
  • As in most other languages one cannot supply unnamed arguments after named ones in the method call.
  • Method cannot have any two arguments with the same name (obviously).

Code example:

foo(a, b::Int=1)="Method 1"

foo(a, b::Float64=1.0)="Method 2"

foo(a, D::String)="Method 3"

foo(1) # Method 2 as before
foo(1, 1.0) # Method 2 as before
foo(a="test") # Method 2
foo(5, b=3) # Method 1
foo(5, "test") # Method 3
foo(D="test") # Error
foo(5, D=1.0) # Error
foo(a=5, b="test") # Error

This design retains backward compatibility. Code unaware of named positional arguments works as before. Code aware of named positional arguments must ensure that positional argument names are consistent (preferable design pattern).
This change should dramatically improve code readability (when used of course) and put it mostly on par with other languages. Also should be relatively easy to implement as mostly relies on existing dispatch logic.
Thanks!

There have been some previous threads about this topic (Allow use of named-argument syntax for positional arguments?, Naming positional arguments at call site), and I daresay it’s a controversial proposal (I’m personally opposed to this.)

I think that in practice, the conclusion on this topic is summarized in the accepted answer from the latest thread:

2 Likes

Thank you for your reply.

I understand however, languages change all the time. And what if changes are non-breaking? Are they also not allowed?

To the design I’m proposing please add one item:

  • When calling a method require the use of semicolon ‘;’ IF a method call references positional arguments by their names.

Code example (had to edit as first version contained some errors):

foo(a, b::Int=1)="Method 1"
foo(a, b::Float64=1.0)="Method 2"
foo(a, D::String)="Method 3"
foo(a, K:Int)="Method 4"
foo(a; K="test")="Method 5"
foo(b::Float64, a::Int)="Method 6"


foo(1) # Method 5 as before
foo(1, 1.0) # Method 2 as before
foo(a="test") # Error (no matching method)
foo(a="test";) # Method 5 (same reason as #1)
foo(5, b=3;) # Method 1
foo(b=3, a=5;) # Method 1
foo(a=5, b=1.0;) # Method 6
foo(1, 2) # Method 4 (as before)
foo(5, "test") # Method 3
foo(D="test";) # Error (no matching method)
foo(5, D=1.0;) # Error (no matching method)
foo(a=5, b="test";) # Error (no matching method)
foo(5, K=5) # Method 5 (as before)
foo(5, K=5;) # Method 4
foo(5; K=5) # Method 5 (as before)
foo(K=5, a=5;) # Method 4

This way we have a clear separation between arguments that participate in dispatch and arguments that don’t with no ambiguities. I tried to find a situation where this change may break anything in the existing code or introduce incompatibility but couldn’t.

The problem with current keyword arguments is they don’t participate in dispatch (which was the right decision, but) and dispatch is the only way to have any polymorphism in Julia, meaning if a function takes only keyword arguments, it can only have one method and have to deal with different arguments in the code.

Please consider this proposition again.
Thanks!

Re @govorunov, I would honestly find the semantics you suggest very confusing and error-prone. Specifically, having two methods with exactly the same type signature would be strage, especially considering that in Julia, typically, the order in which methods of a function are defined is irrelevant, it has no effect on the runtime behavior.

In case of your example:

foo(a, b::Int=1)="Method 1"
foo(a, K:Int)="Method 4"
foo(a; K)="Method 5"

# Why Method 5, why not Method 1? What's the purpose of the default value for b in Method 1?
foo(1) # Method 5 as before 

# Method 1 or Mathod 4? Why?
foo(1, 1) ?

Furthermore, this would be also very-very confusing:

foo(5, K=5) # Method 5
foo(5, K=5;) # Method 4

Not to mention that it would be a breaking change. In Julia, having a semicolon at the end of the parameter list doesn’t change anything. There are no keyword arguments? Fine.

Your suggestions also ignore the intricate behavior of splatting regular and keyword arguments. What would the following calls do?

kwargs = (K = 5,)
f(1, kwargs...)
f(1; kwargs...)
f(1, kwargs...;)

kwargs = (;)
f(1, kwargs...)
f(1; kwargs...)
f(1, kwargs...;)

In Julia, these are all clearly defined, and quite easy to remember. Your recommended modifications would bring a ton of complexity into the system.


If you wan’t to handle various combinations of named parameters in your methods, I suggest you introduce auxiliary structures. E.g.,

Base.@kwdef struct F1
    b::Int = 1
end

Base.@kwdef struct F2
    b::Float64 = 1.0
end

Base.@kwdef struct F3
    D::String
end

Base.@kwdef struct F4
    K::Int
end

f(a, args::F1) = "Method 1"
f(a, args::F2) = "Method 2"
f(a, args::F3) = "Method 3"
f(a, args::F4) = "Method 4"
f(a; K = nothing) = "Method 5"

f(5, F1(b = 3)) # Method 1
f(1, F2(1.0)) # Method 2
f(5, F3("test")) # Method 3
f(5, F4(K = 5)) # Method 4
f(1) # Method 5
f("test") # Method 5
f(5, K = 5) # Method 5
f(5; K = 5) # Method 5

Of course, in the specific implementation, the structs would have more semantic, meaningful names.

Alternatively you can use empty tag structs to select the approriate method:

struct T1 end
struct T2 end

f(::Type{T1}, a, b::Int = 1) = ...
f(::Type{T2}, a, D::String) = ...

f(T1, 1)
f(T2, "foo")

And I’m sure there are many more ways to achieve what you are aiming for in a very Juliaesque way.

2 Likes

Thanks!

Please see my answers below:

I intentionally made the example complex to cover as many edge cases as possible. Normal use would not require any of this.

Yet this is exactly how Julia operates now - you can have two methods with the same signature. The one defined the last will hide previous ones. I just demonstrated this logic remains the same.

AFAIK it has and exactly how it works now. No changes here at all.

Same reason as previous - method 5 hides method 1, exactly as it works now.

Method 4, exactly as it works now - the longest signature wins. Actually, method 5 cannot be called by foo(1, 1) at all as it does not match the signature (second argument is a keyword argument and takes no part in dispatch). Again no change here.

This is just to demonstrate solution for the main problem - disambiguation between keyword and ‘positional’ arguments. In this case foo(5, K=5) is the same as foo(5; K=5) which calls method 5 exactly as it is now. foo(5, K=5;) calls method 4 because semicolon clearly indicates separation between positional and keyword arguments. This is the only new part, but because syntax foo(5, K=5;) is currently invalid in Julia it introduces no breaking changes.

In this design semicolon works exactly as before - separating keyword arguments from arguments that participate in dispatch. The only difference is if one is to reference ‘positional’ (or let’s call it dispatch) arguments by their names, one SHOULD use semicolon to disambiguate from keyword arguments.

Sorry but ugly and unintuitive. Mos other languages have ‘passing arguments by name’ feature. See no point why Julia can’t.

Probably the very reason why Julia struggles to gain traction even though it is such an elegant language. Being proud and unique doesn’t really work that well, people generally don’t care and just want convenient and friendly tools.

But again, with the proposed design I see not a single breaking change. If anyone sees a breaking change I’ll admit my defeat, I otherwise see no reason why this can’t be done. Implementation should be rather straightforward too, although the complexity of the method pattern-matching algorithm has to be increased.

Start a new topic instead next time. Link and quote the comments in an inactive thread you’re replying to. It is more polite to the previous participants and saves the admins the trouble of splitting the topic for you.

Most other languages don’t have multimethods, and that’s the exact reason why methods are dispatched on positional arguments. Trying to mix it up makes multimethods an unfeasible nightmare to write, so it’s only a feasible feature for single-method functions.

You’d change this behavior:

julia> foo(a, b) = 1
foo (generic function with 1 method)

julia> foo(;a, b) = 2
foo (generic function with 2 methods)

julia> foo(b = 1, a = 1;)
2
1 Like

Sorry again. I’m new to discourse and don’t really know how it works.

Could you please provide an example of how the proposed change makes multimethods “an unfeasible nightmare to write”?

Well, the last line should throw an error in the current implementation - you are using a semicolon to start keyword arguments but you’ve already started keyword arguments. The reason it works now is because the compiler is sloppy at enforcing correct usage and just ignores the semicolon. But if you think keeping arguably erroneous but de-facto existing behaviour is more important than improving code readability and maintainability, then yes, it is a breaking change indeed.

Blue words indicate a link, that particular link goes to an example I wrote for a previous topic. The example is more complex and less redundant than yours, but it addresses the issues with method dispatches in calls with named positional arguments.

That is conventionally considered a breaking change, yes. Breaking changes are a well-established concept, anyone’s personal opinion on what it should mean instead is irrelevant. That includes the methods HanD highlighted, if you want a clear demonstration:

julia> foo(a; K)="Method 5"
foo (generic function with 1 method)

julia> foo(a, K::Int)="Method 4" # had to fix your typo K:Int
foo (generic function with 2 methods)

julia> foo(5, K=5)
"Method 5"

julia> foo(5, K=5;)
"Method 5"
1 Like

Is it, though?

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

julia> f(1, b=2;)
3
2 Likes

OK, let’s do this.

Calling foo(1, 2) calls second method as first accepts only one positional argument - no ambiguity here.

Same as

function foo2(p1::Dog, p2::Cat)
function foo2(p2::Dog, p1::Cat)

currently creates ambiguity for positional arguments but I don’t hear people complaining. Of course if one really wants to create untractable code one wouldn’t need many tools, simple C would do just fine.
Same as currently with positional arguments - the last defined method hides the previous, it’s a simple pattern-matching.

Methods are not ‘overwritten’ but hidden. It’s a pattern matching against the list. The same logic applies with the proposed change. If you want to create a mess you don’t need many features to do this.

Does Julia allow keyword arguments preceding positional arguments in calls? Is there any good reason for this?

This should not be allowed at all.

OK, I admit, the foo(5, K=5;) syntax that shouldn’t currently work works in ambiguous way and you wanna keep it as it is. The question is there anyone really using foo(5, K=5;) syntax now because it has no sense?

Same here - shouldn’t currently work, completely useless, but OK, let’s keep the useless ambiguous syntax.

Already admitted - useless, ambiguous, shouldn’t work, but OK, it is a breaking change indeed. Apparently, it is more important to keep it this way.

Those aren’t the same. My example had 2 methods, you changed it to 1 replaced method. You might benefit from reviewing the documentation on methods and running code in the REPL to understand and verify the behavior.

Yes they are, the warning for method overwriting literally says it. The more serious issue there is the quoted excerpt was clearly denoted as a hypothetical language that is not Julia.

Both examples have two methods. In the first example first method is hidden when calling with both parameters by names but disambiguated when calling by position. In the second example the first method is hidden when calling both parameters by position but disambiguates when calling by names. Why first one is a problem and the second one is not?

That is semantic but to prove it try ‘overwriting’ a method in one context (like another function or module) and then call this ‘overwritten’ method after you’ve left the context - you’d call the original method because your ‘overwritten’ method just temporarily hid the original one and didn’t overwrite anything. Essentially it’s just pattern matching against the list of entities in the current context.

The only remaining advice I can offer is that responding to language creators in inactive discourse threads are not how you make a language feature proposal, you do that on Julia’s Github. Discourse is for discussion, and it can be helpful for brainstorming and refining ideas before you make a formal attempt on Github, though it is optional. I for one highly suggest you review Julia and test your claims in an active sessions because making verifiably false claims hastens proposals’ failures, especially when they involve the fundamentals. I’ll do that work for you one last time:

C:\Users\Benny> julia --warn-overwrite=yes

julia> begin
       struct Dog end; struct Cat end
       foo2(p1::Dog, p2::Cat) = 0
       foo2(p2::Cat, p1::Dog) = 1
       fooyours(p1::Dog, p2::Cat) = 0
       fooyours(p2::Dog, p1::Cat) = 1
       nothing
       end
WARNING: Method definition fooyours(Main.Dog, Main.Cat) in module Main at REPL[1]:5 overwritten at REPL[1]:6.

julia> methods(foo2)
# 2 methods for generic function "foo2" from Main:
 [1] foo2(p2::Cat, p1::Dog)
     @ REPL[1]:4
 [2] foo2(p1::Dog, p2::Cat)
     @ REPL[1]:3

julia> methods(fooyours)
# 1 method for generic function "fooyours" from Main:
 [1] fooyours(p2::Dog, p1::Cat)
     @ REPL[1]:6

You’re talking about entirely separate functions with the same name in separate scopes. If you actually overwrite a method, it’s overwritten for good:

julia> greeting() = println("hello"); greeting()
hello

julia> module Blah
         import ..Main: greeting
         greeting() = println("howdy"); greeting()
       end;
WARNING: Method definition greeting() in module Main at REPL[1]:1 overwritten in module Blah at REPL[2]:3.
howdy

julia> greeting()
howdy

julia> function blah()
         global greeting
         @eval greeting() = println("hola")
       end;

julia> blah(); greeting()
WARNING: Method definition greeting() in module Main at REPL[1]:1 overwritten at REPL[3]:3.
hola

I hope these can serve as examples of how you can vet your understanding about Julia on your own, godspeed.

1 Like