Parameters
The caller sends data to the function via parameters. Parameters increase a function's generality and reusability. They are holes left in an algorithm that are filled dynamically by the caller. In some sense, they are like variables that are declared and assigned just as the function is called. For example, suppose you have this C program that rounds a number to the nearest multiple of 10 using a function:
int round10(int x) {
return 10 * (int) round(x / 10.0);
}
int main() {
int nearest10 = round10(17);
printf("%d\n", nearest10);
return 0;
}
The function effectively expands into this program:
int main() {
int nearest10;
{
int x = 17;
nearest10 = 10 * (int) round(x / 10.0);
}
printf("%d\n", nearest10);
return 0;
}
Calling the function introduces a new scope block. At the beginning of the block, the parameter x
is declared and assigned 17. When the block is exited, its local memory is wiped from the call stack, but its return value is copied into the caller's memory.
Formal Parameters
Parameters are the nexus between the caller and the function itself, the callee. They therefore have two sides. The callee sees the parameters as variable declarations that lack initializations. These declarations are a list of formal parameters enumerated in the function's header. They describe the names, types, and other qualifications of the data sent in from the outside. Formal parameters are only hypothetical data. They are placeholders, symbolically representing the real data sent in later by the caller.
The number of formal parameters is the function's arity. Developers of big software often advocate for minimizing arity since function calls with long parameter lists are hard to read.
Some languages allow a formal parameter to be annotated with modifiers that affect its semantics. For example, in C, an array parameter can be qualified as const
to make it immutable:
int sum(const int numbers[], int n) {
int total = 0;
for (int i = 0; i < n; ++i) {
total += numbers[i];
}
// numbers[0] = 19; illegal! numbers is const
return total;
}
This is a helpful feature. Without this guarantee, the caller cannot be certain that the array sent to the function will be the same when the function returns. Java provides no syntactic facility for guaranteeing immutability like C and C++ do. If you pass an array, ArrayList
, or any mutable object to a function, it may come back mutated.
Actual Parameters
The caller sees the parameters as values that it must supply for the function to perform its job. The values sent to a function are the actual parameters, and they are used to initialize the formal parameters. An actual parameter may be any expression that typechecks against the corresponding formal parameter. It may be a simple literal value, a variable from a scope visible to the caller, an operation, or the return value of some other function call. Sometimes new programmers hold the misconception that a formal parameter and an actual parameter have to be variables of the same name, but the actual parameter expression and formal parameter name are independent.
Many languages require the list of actual parameters to be enclosed in parentheses. In Ruby, parentheses are optional, as demonstrated by this script:
numbers = Array.new(3, 0) # [0, 0, 0]
numbers = Array.new 3, 0 # [0, 0, 0]
# All statements print "0,0,0".
puts numbers.join ','.to_s # :( unparenthesized chaining
puts numbers.join(',').to_s() # :| arity 0 parentheses
puts numbers.join(',').to_s # :) readable
Ruby developers tend to adopt a nuanced policy guiding their use of parentheses. When a function call is nested or chained, parentheses are usually included. When a function call has no parameters, as with to_s
, parentheses are often omitted.
Parentheses may also be omitted from the formal parameter list:
def min(a, b)
# ...
end
def max a, b
# ...
end
In Haskell, actual parameters cannot be enclosed in parentheses. This Haskell expression does something very different than you'd expect:
max(5, 6)
Parentheses in Haskell form a tuple. This expression creates the tuple (5, 6)
and then passes it as the only parameter to the max
function. The intended call must have no parentheses and no separating comma:
max 5 6
Positional and Named Parameters
A positional parameter is a parameter whose actual parameter is matched to its formal parameter simply by their corresponding order in the parameter list. C, C++, and Java all use positional parameters exclusively. In this C program, formal parameter a
receives actual parameter 0
, b
receives 1
, and c
receives 2
:
void print3i(int a, int, b, int c) {
printf("%d %d %d\n", a, b, c);
}
int main() {
print3i(0, 1, 2);
return 0;
}
Positional parameters make for very terse function calls. But this terseness places a lot of cognitive load on the programmer, who can't always determine the significance of a parameter by merely looking at the call. Instead the programmer must memorize the function's interface, consult the documentation, or use descriptive variable names. The ordering of parameters is often arbitrary, and programmers struggle to internalize such incidental knowledge.
With named parameters, or keyword parameters, programmers map actual parameters to formal parameters using the names declared in the formal parameter list. The order of parameters is usually relaxed. Ruby allows a parameter to be named instead of positional if it is declared with a succeeding colon in the formal parameter list and assigned an actual parameter using the :
operator:
def inc_wrap(i:, n:)
(i + 1) % n
end
puts inc_wrap(i: 3, n: 10) # prints 4
puts inc_wrap(n: 10, i: 3) # prints 4
# puts inc_wrap(3, 10) # fails with ArgumentError
If a parameter is declared as a named parameter, it must be explicitly named in the actual parameter list. Parameters in Python, on the other hand, are not decorated with any special syntax. They can be named in a call using the =
operator or not named, in which case they are treated as positional parameters:
def inc_wrap(i, n):
return (i + 1) % n
print(inc_wrap(i = 3, n = 10)) # prints 4
print(inc_wrap(n = 10, i = 3)) # prints 4
print(inc_wrap(3, n = 10)) # prints 4
print(inc_wrap(3, 10)) # prints 4
Mixing named and positional parameters leads to confusion about a parameter's position. Most languages therefore allow named parameters to appear only after any positional parameters.
Default Parameters
Parameters make a function more versatile but also more unwieldy, as the they give the caller more work to do. To ease the burden of calling a function, some languages allow programmers to assign default values to the parameters. A parameter with a default value may be omitted from the actual parameters, shortening up the call. This Vector3
class in C++ contains a reset
method that gives each of its three parameters a default value of 0.0
:
class Vector3 {
public:
void reset(double x = 0.0,
double y = 0.0,
double z = 0.0) {
this->x = x;
this->y = y;
this->z = z;
}
private:
double x, y, z;
};
int main(int argc, char **argv) {
Vector3 v;
v.reset(1, 2, 3); // makes (1, 2, 3)
v.reset(1, 2); // makes (1, 2, 0)
v.reset(1); // makes (1, 0, 0)
v.reset(); // makes (0, 0, 0)
return 0;
}
The method may be called with 0, 1, 2, or 3 parameters. The default value is chosen to expedite the common case of zeroing out the vector. Ruby, Python, and JavaScript also support default parameters using similar syntax.
Variadic Functions
Suppose one day you want to find the maximum of two values. No problem. There's a method for that. Then the next day you need the maximum of three values. There's no method for that, so you write one. A week later you need the maximum of four values. Ugh. You could write another method that either takes in four parameters, but this is not sustainable. You could write one that takes in an array, but that forces the caller to assemble the values into an array. Some languages offer another way: variadic functions. A variadic function takes in an arbitrary number of parameters.
You define a varadiac method in Java by appending ...
to the type name of the formal parameter. The type int...
means 0 or more integers. From the callee's perspective, the parameter is an int
array. From the caller's perspective, there's just a comma-separated list of int
values. The programmer who wants to call the method does not assemble the parameters into an array; that is done automatically by the Java virtual machine. This variadic sum
method can be applied to any number of integers:
public static int sum(int... terms) {
// The callee sees terms as an array.
int total = 0;
for (int i = 0; i < terms.length; ++i) {
total += terms[i];
}
return total;
}
public static void main(String[] args) {
// The caller sends just a sequence of numbers.
System.out.println(sum()); // prints 0
System.out.println(sum(1)); // prints 1
System.out.println(sum(1, 2)); // prints 3
System.out.println(sum(1, 2, 3)); // prints 6
System.out.println(sum(1, 2, 3, 4)); // prints 10
}
JavaScript supports variadic functions with similar syntax and array semantics:
function sum(...terms) {
let total = 0;
for (let i = 0; i < terms.length; ++i) {
total += terms[i];
}
return total;
}
console.log(sum()); // prints 0
console.log(sum(1)); // prints 1
console.log(sum(1, 2)); // prints 3
console.log(sum(1, 2, 3)); // prints 6
console.log(sum(1, 2, 3, 4)); // prints 10
Ruby and Python support variadic functions but use *
instead of ...
to mark a variadic parameter. C and C++ support variadic functions too, but not as cleanly and consistently as in other languages.
Variadic functions may also have normal parameters. Since the variadic parameter absorbs as many actual parameters as it can, the normal parameters must be listed first. The show_team
function in this Ruby program accepts a team name and a variadic list of players:
def show_team(team_name, *players)
puts "#{team_name}: #{players.join(', ')}"
end
show_team("The Brute Force", "Hadiza", "Xinyi", "Clark")
Parameters in a variadic list don't have much identity beyond their position in the array. Many classes and functions in the Ruby standard library make use of another feature that gives meaningful names to the variadic parameters: a hash parameter. A function is given a formal parameter that will absorb a list of key-value pairs, like options
in this code:
def configure(ip, options = {})
if options.has_key? 'theme'
# ...
end
if options.has_key? 'autosave'
# ...
end
if options.has_key? 'path'
# ...
end
end
There's nothing variadic in the definition. But there is in the call. The Ruby interpreter automatically bundles any key-value pairs present at the end of the actual parameter list into a hash. These three calls are equivalent:
configure('127.0.0.1', {theme: 'dark', autosave: true})
configure('127.0.0.1', theme: 'dark', autosave: true)
configure '127.0.0.1', theme: 'dark', autosave: true
The hash parameter is a different mechanism than the named parameters discussed earlier, though they achieve a similar purpose of annotating the meaning of actual parameters. The syntactic benefit of a hash parameter is minimal: hash parameters do not need to be enclosed in curly braces. Ruby developers like omitting delimiters.
Spreading an Array
Suppose we have array in a JavaScript program and we need to pass its elements as individual positional parameters to a function. For example, this code for a grid-based game passes row and column coordinates from an array to an attack
function:
function attack(row, column) {
// ...
}
const guess = ['I', 1];
attack(guess[0], guess[1]);
Unpacking the guess
array is a minor annoyance. The JavaScript interpreter will do the unpacking for us if we pass the array intact but precede it with the spread operator:
attack(...guess);
In a formal parameter, the ...
token means that the individual actual parameters will be automatically bundled into an array. In an actual parameter, the ...
means the reverse, that an array will be unpacked to individual parameters.
Arrays may be similarly spread in Ruby using the splat operator:
def attack(row, column)
// ...
end
guess = ['I', 1]
attack(*guess)