Optional positional arguments must occur at end

It seems to me that wanting optional arguments not at end needs unnecessary contortions in Julia.
Here is a typical example:

julia> test(io::IO=stdout,x)=print(io,x)
ERROR: syntax: optional positional arguments must occur at end around REPL[13]:1
Stacktrace:
 [1] top-level scope
   @ REPL[13]:1

one needs to write two methods

julia> test(io::IO,x)=print(io,x)
test (generic function with 1 method)

julia> test(x)=print(stdout,x)
test (generic function with 2 methods)

why does not Julia generate in all cases the two methods, and let ambiguities happen or not happen?

2 Likes

You could have optional positional parameters only at the start too, but then it is just a style consideration (i.e., you gain nothing but changing the order of the parameters). If you have optional positional parameters at both sides of a non-optional positional parameter then all cases are ambiguous except by: providing only the non-optional parameters and providing all parameters (optional and non-optional). Not really useful.

4 Likes

You did not get my point. All cases are ambiguous without type information. But why not let Julia dispatching act. My case above is not ambiguous.

[Off-topic]

Suggested wording for a generous contributor trying to help:
Thank you, but perhaps I’m not making myself clear.

8 Likes

It’s certainly possible but it does tend to generate a lot of methods and makes it really easy to generate ambiguities. Let’s think it through. The natural rule seems to be that an optional positional argument that comes before a required positional argument works like this:

f(a=1, b=2, c) = ...

# means

f(a, b, c) = ...
f(b, c) = f(1, b, c)
f(c) = f(1, 2, c)

That seems ok. So what’s the problem? The problem comes when this feature is combined with trailing optional positional arguments. And if you can do one or the other, it’s only a matter of time before someone asks for the two together. Say we allow that and someone writes this definition:

f(a=1, b=2, c, d=4, e=5) = ...

What would that seemingly simple definition mean? Let’s spell it out:

f(a, b, c, d, e) = ...

# omitting 1 argument
f(a, b, c, d) = f(a, b, c, d, 5)
f(b, c, d, e) = f(1, b, c, d, e)

# omitting 2 arguments
f(a, b, c) = f(a, b, c, 4, 5)
f(b, c, d) = f(1, b, c, d, 5)
f(c, d, e) = f(1, 2, c, d, e)

# omitting 3 arguments
f(b, c) = f(1, b, c, 4, 5)
f(c, d) = f(1, 2, c, d, 5)

# omitting 4 arguments
f(c) = f(1, 2, c, 4, 5)

As you point out, this could be made unambiguous if the arguments are typed in such a way that all of these signatures can be distinguished. But holy moly that seems hard to enforce. We could just not enforce it, of course. After all we don’t prevent ambiguities between different methods. But it’s one thing to allow people to write ambiguous methods with separate bodies, it’s another thing for a method to be so heavily ambiguous with itself! This really seemingly simple method definition has seven ambiguous methods and only two unambiguous ones.

11 Likes

One thing I do think is that it’d maybe be nice if we could have either prefix optional arguments or trailing optional arguments, especially since patterns like

F(f=identity, v)
f(io=stdio, v)

are so common. Of course, this also makes footguns if the programmer themselves write

F(f=identity, x, y) = ...
F(f, x, y=x) = ...

but I also think this is less problematic than the case where they’re allowed to write

F(f=identity, x, y=x)

so it may be worth the convenience as it’s quite annoying to have to constantly write

F(x) = F(identity, x)
F(f, x) = ...
1 Like

That might be fine if each method definition could only have optional leading or trailing positional arguments but not both.

2 Likes

Yeah, that’s what I meant by

I guess I should have said “xor” :laughing:

5 Likes

This is the sign of a spoiled developer (or maybe greedy :wink:)

Because I always thought it was awsome that I could write stuff like that :grinning:

7 Likes

Actually, here’s an interesting idea. We can actually check a set of signatures for ambiguities at definition time. So we could make it an error to write a method definition that is ambiguous with itself. Currently it’s impossible to write such a method, so this would be non-breaking. That would make the f(a=1, b=2, c, d=4, e=5) = ... method definition illegal. But it would allow something like this:

F(f=identity, s::String, n::Integer=0) = ...

This definition would expand to:

F(f, s::String, n::Integer) = ...
F(f, s::String) = F(f, s, 0)
F(s::String, n::Integer) = F(identity, s, n)
F(s::String) = F(identity, s, 0)

Since that set of method signatures isn’t ambiguous, it’s fine. This might actually make it easier to avoid accidental method ambiguities since if you can write the signature with a single method then you’d get an error if it’s ambiguous.

<aside>

We used to emit a warning at method definition time for method ambiguities, back in Julias 0.1-0.3 (IIRC). But this was a huge drag because it was common when multiple packages extended the same generic function (often from Base), for method ambiguities to arise between these extensions. Something like this:

# in Base
f(a::AbstractA, b::AbstractB) = # generic logic

# in PackageX
f(a::SpecificA, b::AbstractB) = # specialization on A

# in PackageY
f(a::AbstractA, b:: SpecificB) = # specialization on B

Now you have an ambiguity if someone calls f(a::SpecificA, b::SpecificB). So loading packages commonly lead to screenfuls of ambiguity warnings, which just made everything seem janky and broken. Of course, if someone does call the ambiguous method, we don’t know what to do, so we throw an error. But it’s fairly common that this intersection method doesn’t make much sense and never gets called, in which case leaving it ambiguous is just fine. Anywho, that’s why we changed method ambiguities from definition-time warnings into runtime errors.

</aside>

17 Likes

One possible/probable indirect effect of this new feature is making even more common to add unnecessary type annotation to the parameters of methods. I do not like this possible outcome very much.

1 Like

If you need the type annotation then the API you wanted—and could already write out with multiple methods—was actually ambiguous, which is worse. You only need to restrict types enough to avoid ambiguity, which one should do anyway.

2 Likes

Thank you very much for your answer. I think this was exactly my idea (which I did not make so clear).

1 Like