Named parameters

In a method call, it can be convenient to label the actual parameters according to the method's formal parameter names. For example, the method void m(int x, int y) {} could be called as m(x:4, y:5). This is especially worthwhile in two cases:


  • When a method has adjacent parameters with the same type but different semantics. For example, Math.pow(double,double) might be easier to use if declared as double pow(double raise, double toThePower) {} and called as pow(raise: 4, toThePower: 5). An entrypoint to a banking application void login(boolean showBalance, boolean showOffers, boolean trackActivity) could be called as login(showBalance: getUserPrefs(), showOffers: true, trackActivity: false).

  • When a method has many parameters, regardless of their type. Too many parameters indicates misdesign - one of the Epigrams of Programming is "If you have a procedure with 10 parameters, you probably missed some" - but it would never do to impose a hard limit, so inline documentation from named parameters is the best option.

Named parameters raise some interesting questions:


  • If both actual and formal parameters are named, can the actual parameters be reordered w.r.t. the formal parameters?
  • Must all actual parameters be named, or only some?

If parameter order is fixed, then names could potentially be omitted. Consider the method:

void m(int x, int y, int z) {}

It's not difficult to match actual parameters to formal parameters, even without names:

m(x:1, y:2, 3) or
m(1, 2, z:3)

But if parameter order is variable, then omitting names is a disaster in the making. The call:

m(z:3, 1, 2)

makes you work to realize that x binds to 1 and y to 2. In the worse case, you destroy the convenience of reordering because you must match all actual parameters to formal parameters to understand the bindings, as in:

m(y:2, 1, x:3)

So, allowing reordering is the crucial question. Some will say that the whole point of named parameters is to aid readability of the caller in the context of an obtuse or verbose callee, and that reordering can improve readability. Others will disagree. Personally, I think the dissonance is greater when some parameters are named and some are not, than when all parameters are named but given out of order. Therefore, I would like to allow reordering and disallow omission.

Reordering actually has a profitable interaction with variable-arity methods. Consider:

void m(int x, int... y) {}

It would be nice to call it as:

m(x:1, y:2, y:3) or
m(x:1, y:{2,3})

Allowing reordering would allow the vararg parameter to come first:

m(y:2, y:3, x:1) or
m(y:{2,3}, x:1)

or even allow the vararg parameter to be distributed:

m(y:2, x:1, y:3)

This undoes the tradition of variable-arity parameters coming last, but then, they're only last because in any other position and without names, you can't differentiate a variable-arity actual parameter from a fixed-arity actual parameter. Named parameters with reordering make the last-position requirement unnecessary, and also the requirement for only one vararg per method. It would be quite reasonable to declare:

void m(int... x, int... y, int z) {}

and call:

m(x:1, y:100, x:2, y:200, x:3, y:300, z:1000)

if there is a natural association of x values with y values.

Clearly, reordering is a powerful concept. That usually means complexity. Let's look at how method call works, and how named parameters with or without reordering would affect it.

Into the heart of darkness: Overload resolution

Method resolution is the process of matching the method name and set of actual parameters supplied in a call to a method declaration in the receiver's class. If successful, the method call is resolved. Because the Java language and virtual machine support method overloading, a Java compiler uses the actual parameter types of a method call to select a single method declaration (obviously with the right name) with the "best" matching formal parameter types. This process is called overload resolution. It is rather complex because overloading is tricky in principle and because methods can be generic, of variable arity, and have formal parameters which require boxing/unboxing conversion of the actual parameters. On the bright side, the complexity is only at compile-time, because at run-time, the JVM's invokevirtual instruction simply calls a method with the exact name and formal parameter types chosen by the compiler. The important point is that the language and VM both use formal parameter types to resolve a method call. Static method resolution (i.e. overload resolution) and dynamic method resolution (i.e. invokevirtual's lookup) are aligned.

To involve parameter names as well as parameter types, there are two fundamental approaches. One is to use names as a simple static sanity check and leave static and dynamic resolution untouched. The other is to aggressively thread names through static and dynamic resolution. Let's compare the approaches.

Conservative

The conservative approach is to leave overload resolution unchanged, and add a final step to check that the name of each actual parameter matches the name of the corresponding formal parameter in the resolved method. Formal parameter names are not stored in classfiles today, but there is an RFE to make them available at runtime, and the first step would be to reify them in the classfile. Let's assume that's been done, and that a Java compiler can see the formal parameter names of any method declaration. Of course, if the resolved method is in a legacy classfile without formal parameter names, then the name-matching step must be skipped, but that's OK because it was only a sanity check anyway. The logic of this approach is simple, at the cost of not allowing reordering of actual parameters.

Aggressive

The aggressive approach is to alter overload resolution when it identifies the set of potentially applicable methods. The set would consist of precisely those methods whose formal parameter names match those at the caller, up to ordering and varargs (see below). There would never be a compile-time error about a call using an actual parameter name which is not a formal parameter name of the resolved method, because the set of potentially applicable methods is correct by construction. Which potentially applicable method is resolved is up to the actual and formal parameter types as usual.

This approach is distinctly unfriendly to migration compatibility, because compiling against a legacy classfile, without formal parameter names, will mean there are no potentially applicable methods for the call. Dropping back to traditional overload resolution based on classfile version is ugly, and having to ignore actual parameter names when they were intended to play a central role in resolution is repugnant.

Perhaps being aggressive is worthwhile if it allows richer overloadings than today, based on names as well as types? Currently, overloadings "erase" formal parameter names and are legal up to formal parameter types:

void m(Object x, String y) {} // m(Object,String)
void m(String x, Object y) {} // m(String,Object)

The following is illegal because both signatures "erase" to m(Object,String) (they are override-equivalent in JLS terms) so a call which does not use named parameters cannot differentiate between them:

void m(Object x, String y) {} // m(Object,String)
void m(Object y, String x) {} // m(Object,String)

You might say this is a shame, and that override-equivalence should take formal parameter names into account, because a call using named parameters can differentiate:

m(x:new Object(), y:"hi") // resolves to m(Object x, String y)
m(x:"hi", y:new Object()) // resolves to m(Object y, String x)

However, migration compatibility dictates that we can't assume all calls will used named parameters, and that it is unreasonable for aggressive overloadings which assume such callers to break other callers. Therefore, the overloading must stay illegal and we may as well keep static method resolution based on types. This is just as well, because some legal type-based overloadings cause problems for the aggressive named-based approach. Consider these methods:

void m(Object x, String y) {} // m(Object,String)
void m(String y, Object x) {} // m(String,Object)

and this call:

m(x:new Object(), y:"hi")

Its set of potentially applicable methods contains duplicates:

void m(Object x, String y) {} // m(Object,String)
void m(Object x, String y) {} // m(String,Object) after shuffling formal parameters to match the actual parameter ordering

The call is thus ambiguous. It makes no sense to aggressively change overload resolution to use names when doing so inherently rules out the use of names to call some legacy methods.

Since we say that no two methods can have formal parameters with the same types and different names, dynamic method resolution need not change. This is convenient because allowing name-and-type-based overloadings would mean significant VM changes. Today, a classfile cannot store duplicate methods (same name and formal parameter types) and the invokevirtual instruction could not differentiate between them in any case. A compiler would need to use invokedynamic to accurately resolve such methods.

As a matter of interest, is it possible to use names and types at static method resolution and then erase the names (akin to generics), so types alone are used at dynamic resolution? Not if you want to stay sane. Consider these methods:

void m(Number x) {}
void m(Integer y) {}

and the call:

m(x:5)

which statically resolves to m(Number). Changing the name of its formal parameter:

void m(Number z) {} // z not x
void m(Integer y) {}

means the types still match at runtime (i.e. it's a binary-compatible change) but recompiling the call would give a compile-time error because no methods are potentially applicable. This discrepancy is typical of features implemented by erasure. And not recompiling the call means invokevirtual now targets a less-specific method in terms of type (m(Number) rather than m(Integer)) for a reason (compile-time belief in the presence of an x formal parameter) which no longer holds. More discrepancy.

Finally, note the oddity that m(x:5) resolves to m(Number) while m(5) resolves to m(Integer). It seems that the aggressive approach is dead.

In conclusion, named parameters are possible in Java, but reordering - a considerable benefit - is incompatible with a practical design that preserves high levels of compatibility and usability. Nevertheless, named parameters increase readability where it's needed most, and would be an interesting addition to the language.

Thanks to Jon Gibbons, Maurizio Cimadamore, and Keith McGuigan for feedback and assistance.

Comments:

Here's two of ways I've previously thought about getting that information across with current Java:

login(/\*showBalance:\*/ getUserPrefs(), /\*showOffers:\*/ true, /\*trackActivity:\*/ false)

login(
getUserPrefs(), // showBalance
true, // showOffers
false // trackActivity
);

Editor syntax highlighting might make it more readable.

Posted by Simon Hartley on September 05, 2008 at 08:52 PM PDT #

I'd rather put effort into making it simple to define simple classes. If you are in a position where you need named parameters for your methods, then you can always define a class with a set of named properties and accept an instance of this class as your sole argument. It always works better in all respects, including your ability to extend the number of parameters in the future in a backward-compatible way.
If a language has a provision to define such class in a few lines, then the need to support named parameters is gone.

Posted by Roman Elizarov on September 07, 2008 at 04:40 PM PDT #

Following on from Simon's comment. You can do well at the moment:

// Arguments and \*\*\*default\*\*\* vales
public static class MValues { public int x = 1, y = 2, z = 3; }

...

m( new MValues(){{ x = 2; y = 4; z = 8; }} );

Is this worth the trouble? Short syntax for creating inner classes would have a much larger use case, e.g. C3S:

http://www.artima.com/weblogs/viewpost.jsp?thread=182412

Posted by Howard Lovatt on September 07, 2008 at 07:14 PM PDT #

"If you have a procedure with ten parameters, you probably missed some."

Please give credit where it is due. The quote is from the article "Epigrams in Programming" by Alan J. Perlis.

Posted by Trevor on September 07, 2008 at 10:47 PM PDT #

I am pleased that reordering is causing problems. I just doesn't seem right to me. Another problem with reordering is that changing the name of a formal parameter would no longer have no impact on pre-existing binaries. (see JLS 13.4.14), that is, it would no longer be a binary compatible change.

Given that we can only choose one of reordering or optional naming, then optional naming meets my "needs" much more than reordering does.

Posted by Bruce Chapman on September 08, 2008 at 03:03 PM PDT #

Post a Comment:
Comments are closed for this entry.
About

Alex Buckley is the Specification Lead for the Java language and JVM at Oracle.

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today
Feeds