Naming positional arguments at call site

That PR seems strange to me because it’s about naming positional arguments but then compares it to Python, which doesn’t have named positional arguments either. Python has positional-or-keyword arguments, which are different because when a keyword is provided in the call, the argument is matched using the keyword, never the position e.g. foo(id=1, mass=5.6) == foo(mass=5.6, id=1). To be matched by position, an argument cannot have a keyword in the call.

When comparing Julia and Python in particular, it’s illustrative to compare how methods specify which arguments are which and why. This more or less just sums up what had been discussed in the thread earlier in one place.

Julia methods simply divide positional arguments from keyword arguments with ;. Positional-or-keyword arguments are impossible because multimethods vary on the number and annotated type of positional-only arguments:

# p_ - match position
# k_ - match keyword provided in call

# calling foo(1, 2) would be ambiguous if k1 could be positional
function foo(p1; k1)
function foo(p1, p2)

# matching keywords in a call e.g. foo2(p2=Cat(), p1=Dog()) would be ambiguous
function foo2(p1::Dog, p2::Cat)
function foo2(p2::Cat, p1::Dog)

In a hypothetical language, types could be matched by keyword instead, but that would only make multimethods dispatch on keyword-only arguments, not allow positional-or-keyword arguments:

# not Julia: foo(k1=Dog(), k2=Cat()) != foo(k2=Dog(), k1=Cat())
# matching positions in a call e.g. foo(Dog(), Cat()) would be ambiguous
function foo(k1::Dog, k2::Cat)
function foo(k2::Dog, k1::Cat)

function foo(k2::Cat, k1::Dog) # overwrites the 1st method
function foo(k1::Cat, k2::Dog) # overwrites the 2nd method

The only way to resolve that positional ambiguity is force an order on k1, k2 in the methods, so types can be matched on position and keywords. But now we’re forced to name and order arguments consistently across all methods, e.g. a nonsensical greet(dog::Cat, cat::Dog) after greet(dog::Dog, cat::Cat). This is a nightmare, so types were chosen to match by position, where calls are simpler to write.

Python can allow positional-or-keyword arguments because of its “unimethods,” though it must in turn disallow keyword arguments preceding positional arguments in calls and adds complexity (*args, *, /) to dividing the arguments:

# pk_ - match keyword if provided in call, position otherwise

def foo1(pk1, pk2): # calling foo1(1, pk1=2) specifies pk1 twice and errors
def foo2(p1, p2, *pv, k1, k2): # if pv is empty, p1, p2 --> pk1, pk2
def foo3(pk1, pk2, *, k1, k2): # PEP 3102, v3.0+
def foo4(p1, p2, /, pk1, pk2, *, k1, k2): # PEP 570, v3.8+

Since Python already has keyword-able arguments, it doesn’t have named positional arguments because it’d look indistinguishable in calls and clash with the positional-preceding-keyword rule. Julia doesn’t have named positional arguments because the indistinguishability results in dispatch ambiguity:

function foo3(x, y)
function foo3(y; x)
# foo3(x=1, 2) calls the latter, would be ambiguous otherwise
3 Likes