I was just thinking how much easier it would be to read code if, when calling a function, you could write:
doFunction(param1=something, param2=somethingElse);
I can’t think of any drawbacks and it would make code a lot more readable. I know you could pass an array as the only argument and have the array keys as the parameter names, however that would still not be as readable.
Is there a disadvantage of this that I’m missing? If not, why do many languages not allow this?
7
Named Parameters Make Code Easier To Read, Harder To Write
When I am reading a piece of code, named parameters can introduce context that makes the code easier to understand. Consider for example this constructor: Color(1, 102, 205, 170)
. What on earth does that mean? Indeed, Color(alpha: 1, red: 102, green: 205, blue: 170)
would be far easier to read. But alas, the Compiler says “no” – it wants Color(a: 1, r: 102, g: 205, b: 170)
. When writing code using named parameters, you spend an unnecessary amount of time looking up the exact names – it is easier to forget the exact names of some parameters than it is to forget their order.
This once bit me when using a DateTime
API that had two sibling classes for points and durations with almost identical interfaces. While DateTime->new(...)
accepted an second => 30
argument, the DateTime::Duration->new(...)
wanted seconds => 30
, and similar for other units. Yes, it absolutely makes sense, but this showed me that named parameters ≠ ease of use.
Bad Names Don’t Even Make It Easier To Read
Another example how named parameters can be bad is probably the R language. This piece of code creates a data plot:
plot(plotdata$n, plotdata$mu, type="p", pch=17, lty=1, bty="n", ann=FALSE, axes=FALSE)
You see two positional arguments for the x and y data rows, and then a list of named parameters. There are many more options with defaults, and only those are listed whose defaults I wanted to change or explicitly specify. Once we ignore that this code uses magic numbers, and could benefit from using enums (if R had any!), the issue is that many of these parameter names are rather indecipherable.
pch
is actually the plot character, the glyph that will be drawn for every data point.17
is an empty circle, or something like that.lty
is the line type. Here1
is a solid line.bty
is the box type. Setting it to"n"
avoids a box being drawn around the plot.ann
controls the appearance of axis annotations.
For someone who does not know what each abbreviation means, these options are rather confusing. This also reveals why R uses these labels: Not as self-documenting code, but (being a dynamically typed language) as keys to map the values to their correct variables.
Properties Of Parameters And Signatures
Function signatures may have the following properties:
- Arguments can be ordered or unordered,
- named or unnamed,
- required or optional.
- Signatures can also be overloaded by size or type,
- and can have an unspecified size with varargs.
Different languages land at different coordinates of this system. In C, arguments are ordered, unnamed, always required, and can be varargs. In Java the situation is similar, except that signatures can be overloaded. In Objective C, signatures are ordered, named, required, and can’t be overloaded because it’s just syntactic sugar around C.
Dynamically typed languages with varargs (command line interfaces, Perl, …) can emulate optional named parameters. Languages with signature size overloading have something like positional optional parameters.
How Not To Implement Named Parameters
When thinking of named parameters, we usually assume named, optional, unordered parameters. Implementing these is difficult.
Optional parameters could have default values. These must be specified by the called function and should not be compiled into the calling code. Otherwise, the defaults can’t be updated without recompiling all dependent code.
Now an important question is how the arguments are actually passed to the function. With ordered parameters, the args can be passed in a register, or in their inherent order on the stack. When we exclude the registers for a moment, the problem is how to put unordered optional arguments onto the stack.
For that, we need some order over the optional arguments. What if the declaration code is changed? As the order is irrelevant, a reordering in the function declaration should not change the position of the values on the stack. We should also consider if adding a new optional parameter is possible. From a users perspective this seems so, because code that didn’t use that parameter previously should still work with the new parameter. So this excludes orderings like using the order in the declaration or using the alphabetic order.
Consider this also in the light of subtyping and the Liskov Substitution Principle – in the compiled output, the same instructions should be able to invoke the method on a subtype with possibly new named parameters, and on a supertype.
Possible Implementations
If we cannot have a definitive order, so we need some unordered data structure.
-
The simplest implementation is to simply pass the name of the parameters along with the values. This is how named params are emulated in Perl or with command line tools. This solves all extension problems mentioned above, but can be a huge waste of space – not an option in performance-critical code. Also, processing these named params is now much more complicated than simply popping values off a stack.
Actually, the space requirements can be reduced by using string pooling, which can reduce later string comparisons to pointer comparisons (except when it can’t be guaranteed that static strings are actually pooled, in which case the two strings will have to be compared in detail).
-
Instead, we could also pass a clever data structure that works as a dictionary of named arguments. This is cheap on the caller side, because the set of keys is statically known at the call location. This would allow to create a perfect hash function or to precalculate a trie. The callee will still have to test for existence of all possible parameter names which is somewhat expensive. Something like this is used by Python.
So it’s just too expensive in most cases
If a function with named parameters is to be properly extensible, a definitive ordering cannot be assumed. So there are only two solutions:
- Make the order of the named params part of the signature, and disallow later changes. This is useful for self-documenting code, but does not help with optional arguments.
- Pass a key-value data structure to the callee, which then has to extract useful information. This is very expensive in comparision, and usually only seen in scripting languages without an emphasis on performance.
Other Pitfalls
The variable names in a function declaration usually have some internal meaning and are not part of the interface – even if many documentation tools will still show them. In many cases you’d want different names for an internal variable and the corresponding named argument. Languages that don’t allow choosing the externally visible names of a named parameter don’t gain much of them if the variable name isn’t used with the calling context in mind.
An issue with emulations of named arguments is the lack of static checking on the caller side. This is especially easy to forget when passing a dictionary of arguments (looking at you, Python). This is important because passing a dictionary is a common workaround, e.g. in JavaScript: foo({bar: "baz", qux: 42})
. Here, neither the types of the values nor the existence or absence of certain names can be checked statically.
Emulating Named Parameters (In Statically Typed Languages)
Simply using strings as keys, and any object as a value is not very useful in the presence of a static type checker. However, named arguments can be emulated with structs, or object literals:
// Java
static abstract class Arguments {
public String bar = "default";
public int qux = 0;
}
void foo(Arguments args) {
...
}
/* using an initializer block */
foo(new Arguments(){{ bar = "baz"; qux = 42; }});
4
Specifying parameter names does not always make code more readable: as an example, would you prefer to read min(first=0, second=1)
or min(0, 1)
?
If you accept the previous paragraph’s argument, then it is pretty obvious that specifying parameter names should not be mandatory. Why don’t all languages make specifying parameter names a valid option?
I can think of at least two reasons:
- In general, introducing a second syntax for an already-covered need (here, passing parameters) is something whose benefits must be balanced against the inherent cost of the introduced language complexity (both in language implementation and in the programmer’s mind model of the language);
- It makes changing parameter names an API breaking change, which may not conform to a developer’s expectation that changing a parameter name is a trivial, non-breaking change;
Note that I do not know of any language that implements named parameters without also implementing optional parameters (which require named parameters) – so you should be wary of overestimating their readability benefits, which can be more consistently obtained with the definition of Fluent Interfaces.
4
For the same reason that Hungarian Notation is no longer widely used; Intellisense (or its moral equivalent in non-Microsoft IDE’s). Most modern IDE’s will tell you everything you need to know about a parameter by merely hovering over the parameter reference.
That said, a number of languages do support named parameters, including C# and Delphi. In C#, it is optional, so you don’t have to use it if you don’t want to, and there are other ways to specifically declare members, like object initialization.
Named Parameters are mostly useful when you only want to specify a subset of optional parameters, and not all of them. In C#, this is very useful because you no longer need a bunch of overloaded methods to provide the programmer with this flexibility.
1
Named parameters are a solution to a problem in refactoring source code and not intended to make source code more readable. Named parameters are used to assist the compiler/parser in resolving default values for function calls. If you want to make code more readable, than add meaningful comments.
Languages that are difficult to refactor often support named parameters, because foo(1)
is going to break when the signature of foo()
changes, but foo(name:1)
is less likely to break requiring less effort from the programmer to make changes to foo
.
What do you do when you have to introduce a new parameter to the following function, and there are hundreds or thousands of lines of code calling that function?
foo(int weight, int height, int age = 0);
Most programmers will do the following.
foo(int weight, int height, int age = 0, string gender = null);
Now, no refactoring is required and the function can execute in legacy mode when gender
is null.
To specific gender
a value is now HARDCODED into the call for age
. As this example:
foo(10,10,0,"female");
The programmer looked at the function definition, saw that age
has a default value of 0
and used that value.
Now the default value for age
in the function definition is completely useless.
Named parameters allow you to avoid this problem.
foo(weight: 10, height: 10, gender: "female");
New parameters can be added to foo
and you don’t have to worry about the order they were added in and you can change default values knowing that the default you set is really it’s true default value.
Bash certainly supports this style of programming, and both Perl and Ruby support passing name/value parameter lists (as would any language with native hash/map support). Nothing stops you from choosing to define a struct/record or hash/map which attaches names to parameters.
For languages which include hash/map/keyvalue stores natively, you can get the functionality you want, simply by adopting the idiom to pass key/value hashes to your functions/methods. Once you have tried it on a project, you will have better insight into whether you have gained anything, either from increased productivity derived from ease of use, or improved quality through reduced defects.
Note that experienced programmers have become comfortable with passing parameters by order/slot. You may also find that this idiom is only valuable when you have more than a few arguments (say >5/6). And since a large number of arguments is often indicative of (overly) complex methods, you may find that this idiom is only beneficial for the most complex of methods.
1
I think it’s the same reason why c# is more popular than VB.net. While VB.NET is more “readable” eg you type “end if” instead of a closing bracket, it just ends up being more stuff that pads out the code and eventually makes it harder to understand.
It turns out that what makes code easier to understand is conciseness. The less the better. Function parameter names are normally pretty obvious anyway, and don’t really go that far into helping you understand code.
1