Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

Function Parameters

This chapter covers various kinds of function parameters.

Some of the concepts of this chapter have already appeared earlier in the book. For example, the ref keyword that we saw in the foreach Loop chapter was making actual elements available in foreach loops as opposed to copies of those elements.

Additionally, we covered the const and immutable keywords and the differences between value types and reference types in previous chapters.

We have written functions that produced results by making use of their parameters. For example, the following function uses its parameters in a calculation:

double weightedAverage(double quizGrade, double finalGrade)
{
    return quizGrade * 0.4 + finalGrade * 0.6;
}

That function calculates the average grade by taking 40% of the quiz grade and 60% of the final grade. Here is how it may be used:

    int quizGrade = 76;
    int finalGrade = 80;

    writefln("Weigthed average: %2.0f",
             weightedAverage(quizGrade, finalGrade));
Parameters are always copied

In the code above, the two variables are passed as arguments to weightedAverage(). The function uses its parameters. This fact may give the false impression that the function uses the actual variables that have been passed as arguments. In reality, what the function uses are copies of those variables.

This distinction is important because modifying a parameter changes only the copy. This can be seen in the following function that is trying to modify its parameter (i.e. making a side effect). Let's assume that the following function is written for reducing the energy of a game character:

void reduceEnergy(double energy)
{
    energy /= 4;
}

Here is a program that tests reduceEnergy():

import std.stdio;

void reduceEnergy(double energy)
{
    energy /= 4;
}

void main()
{
    double energy = 100;

    reduceEnergy(energy);
    writeln("New energy: ", energy);
}

The output:

New energy: 100     ← Not changed

Although reduceEnergy() drops the value of its parameter to a quarter of its original value, the variable energy in main() does not change. The reason for this is that the energy variable in main() and the energy parameter of reduceEnergy() are separate; the parameter is a copy of the variable in main().

To observe this more closely, let's insert some writeln() expressions:

import std.stdio;

void reduceEnergy(double energy)
{
    writeln("Entered the function      : ", energy);
    energy /= 4;
    writeln("Leaving the function      : ", energy);
}

void main()
{
    double energy = 100;

    writeln("Calling the function      : ", energy);
    reduceEnergy(energy);
    writeln("Returned from the function: ", energy);
}

The output:

Calling the function      : 100
Entered the function      : 100
Leaving the function      : 25   ← the parameter changes,
Returned from the function: 100  ← the variable remains the same
Referenced variables are not copied

Even parameters of reference types like slices, associative arrays, and class variables are copied to functions. However, the original variables that are referenced (i.e. elements of slices and associative arrays, and class objects) are not copied. Effectively, such variables are passed to functions as references: the parameter becomes another reference to the original object. It means that a modification made through the reference modifies the original object as well.

Being slices of characters, this applies to strings as well:

import std.stdio;

void makeFirstLetterDot(dchar[] str)
{
    str[0] = '.';
}

void main()
{
    dchar[] str = "abc"d.dup;
    makeFirstLetterDot(str);
    writeln(str);
}

The change made to the first element of the parameter affects the actual element in main():

.bc

However, the original slice and associative array variables are still passed by copy. This may have surprising and seemingly unpredictable results unless the parameters are qualified as ref themselves.

Surprising reference semantics of slices

As we saw in the Slices and Other Array Features chapter, adding elements to a slice may terminate element sharing. Obviously, once sharing ends, a slice parameter like str above would not be a reference to the elements of the passed-in original variable anymore.

For example, the element that is appended by the following function will not be seen by the caller:

import std.stdio;

void appendZero(int[] arr)
{
    arr ~= 0;
    writefln("Inside appendZero()       : %s", arr);
}

void main()
{
    auto arr = [ 1, 2 ];
    appendZero(arr);
    writefln("After appendZero() returns: %s", arr);
}

The element is appended only to the function parameter, not to the original slice:

Inside appendZero()       : [1, 2, 0]
After appendZero() returns: [1, 2]    ← No 0

If the new elements need to be appended to the original slice, then the slice must be passed as ref:

void appendZero(ref int[] arr)
{
    // ...
}

The ref qualifier will be explained below.

Surprising reference semantics of associative arrays

Associative arrays that are passed as function parameters may cause surprises as well because associative arrays start their lives as null, not empty.

In this context, null means an uninitialized associative array. Associative arrays are initialized automatically when their first key-value pair is added. As a consequence, if a function adds an element to a null associative array, then that element cannot be seen in the original variable because although the parameter is initialized, the original variable remains null:

import std.stdio;

void appendElement(int[string] aa)
{
    aa["red"] = 100;
    writefln("Inside appendElement()       : %s", aa);
}

void main()
{
    int[string] aa;    // ← null to begin with
    appendElement(aa);
    writefln("After appendElement() returns: %s", aa);
}

The original variable does not have the added element:

Inside appendElement()       : ["red":100]
After appendElement() returns: []    ← Still null

On the other hand, if the associative array were not null to begin with, then the added element would be seen by the caller as well:

    int[string] aa;
    aa["blue"] = 10;  // ← Not null before the call
    appendElement(aa);

This time the added element is seen by the caller:

Inside appendElement()       : ["red":100, "blue":10]
After appendElement() returns: ["red":100, "blue":10]

For that reason, it may be better to pass the associative array as a ref parameter, which will be explained below.

Parameter qualifiers

Parameters are passed to functions according to the general rules described above:

Those are the default rules that are applied when parameter definitions have no qualifiers. The following qualifiers change the way parameters are passed and what operations are allowed on them.

in

We have seen that functions can produce values and can have side effects. The in keyword specifies that the parameter is going be used only as input. Such parameters cannot be modified by the function as its side effects:

import std.stdio;

double weightedTotal(in double currentTotal,
                     in double weight,
                     in double addend)
{
    return currentTotal + (weight * addend);
}

void main()
{
    writeln(weightedTotal(1.23, 4.56, 7.89));
}

in parameters cannot be modified:

void foo(in int value)
{
    value = 1;    // ← compilation ERROR
}

in is the equivalent of const scope.

out

We know that functions return what they produce as their return values. The fact that there is only one return value is sometimes limiting as some functions may need to produce more than one result. (Note: It is possible to return more than one result by defining the return type as a Tuple or a struct. We will see these features in later chapters.)

The out keyword makes it possible for functions to return results through their parameters. When out parameters are modified within the function, those modifications affect the original variable that has been passed to the function. In a sense, the assigned value goes out of the function through the out parameter.

Let's have a look at a function that divides two numbers and produces both the quotient and the remainder. The return value is used for the quotient and the remainder is returned through the out parameter:

import std.stdio;

int divide(in int dividend, in int divisor, out int remainder)
{
    remainder = dividend % divisor;
    return dividend / divisor;
}

void main()
{
    int remainder;
    int result = divide(7, 3, remainder);

    writeln("result: ", result, ", remainder: ", remainder);
}

Modifying the remainder parameter of the function modifies the remainder variable in main() (their names need not be the same):

result: 2, remainder: 1

Regardless of their values at the call site, out parameters are first assigned to the .init value of their types automatically:

import std.stdio;

void foo(out int parameter)
{
    writeln("After entering the function      : ", parameter);
}

void main()
{
    int variable = 100;

    writeln("Before calling the function      : ", variable);
    foo(variable);
    writeln("After returning from the function: ", variable);
}

Even though there is no explicit assignment to the parameter in the function, the value of the parameter automatically becomes the initial value of int, affecting the variable in main():

Before calling the function      : 100
After entering the function      : 0  ← the value of int.init
After returning from the function: 0

As this demonstrates, out parameters cannot pass values into functions; they are strictly for passing values out of functions.

We will see in later chapters that returning Tuple or struct types are better alternatives to out parameters.

const

As we saw earlier, const guarantees that the parameter will not be modified inside the function. It is helpful for the programmers to know that certain variables will not be changed by a function. const also makes functions more useful by allowing const, immutable, and mutable variables to be passed through that parameter:

import std.stdio;

dchar lastLetter(const dchar[] str)
{
    return str[$ - 1];
}

void main()
{
    writeln(lastLetter("constant"));
}
immutable

As we saw earlier, immutable makes functions require that certain variables must be immutable. Because of such a requirement, the following function can only be called with strings with immutable elements (e.g. string literals):

import std.stdio;

dchar[] mix(immutable dchar[] first,
            immutable dchar[] second)
{
    dchar[] result;
    int i;

    for (i = 0; (i < first.length) && (i < second.length); ++i) {
        result ~= first[i];
        result ~= second[i];
    }

    result ~= first[i..$];
    result ~= second[i..$];

    return result;
}

void main()
{
    writeln(mix("HELLO", "world"));
}

Since it forces a requirement on the parameter, immutable parameters should be used only when immutability is required. Otherwise, in general const is more useful because it accepts immutable, const, and mutable variables.

ref

This keyword allows passing a variable by reference even though it would normally be passed as a copy (i.e. by value).

For the reduceEnergy() function that we saw earlier to modify the original variable, it must take its parameter as ref:

import std.stdio;

void reduceEnergy(ref double energy)
{
    energy /= 4;
}

void main()
{
    double energy = 100;

    reduceEnergy(energy);
    writeln("New energy: ", energy);
}

This time, the modification that is made to the parameter changes the original variable in main():

New energy: 25

As can be seen, ref parameters can be used both as input and output. ref parameters can also be thought of as aliases of the original variables. The function parameter energy above is an alias of the variable energy in main().

Similar to out parameters, ref parameters allow functions to have side effects as well. In fact, reduceEnergy() does not return a value; it only causes a side effect through its single parameter.

The programming style called functional programming favors return values over side effects, so much so that some functional programming languages do not allow side effects at all. This is because functions that produce results purely through their return values are easier to understand, implement, and maintain.

The same function can be written in a functional programming style by returning the result, instead of causing a side effect. The parts of the program that changed are highlighted:

import std.stdio;

double reducedEnergy(double energy)
{
    return energy / 4;
}

void main()
{
    double energy = 100;

    energy = reducedEnergy(energy);
    writeln("New energy: ", energy);
}

Note the change in the name of the function as well. Now it is a noun as opposed to a verb.

auto ref

This qualifier can only be used with templates. As we will see in the next chapter, an auto ref parameter takes lvalues by reference and rvalues by copy.

inout

Despite its name consisting of in and out, this keyword does not mean input and output; we have already seen that input and output is achieved by the ref keyword.

inout carries the mutability of the parameter to the return type. If the parameter is const, immutable, or mutable; then the return value is also const, immutable, or mutable; respectively.

To see how inout helps in programs, let's look at a function that returns a slice to the inner elements of its parameter:

import std.stdio;

int[] inner(int[] slice)
{
    if (slice.length) {
        --slice.length;               // trim from the end

        if (slice.length) {
            slice = slice[1 .. $];    // trim from the beginning
        }
    }

    return slice;
}

void main()
{
    int[] numbers = [ 5, 6, 7, 8, 9 ];
    writeln(inner(numbers));
}

The output:

[6, 7, 8]

According to what we have established so far in the book, in order for the function to be more useful, its parameter should be const(int)[] because the elements are not being modified inside the function. (Note that there is no harm in modifying the parameter slice itself, as it is a copy of the original variable.)

However, defining the function that way would cause a compilation error:

int[] inner(const(int)[] slice)
{
    // ...
    return slice;    // ← compilation ERROR
}

The compilation error indicates that a slice of const(int) cannot be returned as a slice of mutable int:

Error: cannot implicitly convert expression (slice) of type
const(int)[] to int[]

One may think that specifying the return type as const(int)[] would be the solution:

const(int)[] inner(const(int)[] slice)
{
    // ...
    return slice;    // now compiles
}

Although the code now compiles, it brings a limitation: even when the function is called with a slice of mutable elements, this time the returned slice ends up consisting of const elements. To see how limiting this would be, let's look at the following code, which tries to modify the inner elements of a slice:

    int[] numbers = [ 5, 6, 7, 8, 9 ];
    int[] middle = inner(numbers);    // ← compilation ERROR
    middle[] *= 10;

The returned slice of type const(int)[] cannot be assigned to a slice of type int[], resulting in an error:

Error: cannot implicitly convert expression (inner(numbers))
of type const(int)[] to int[]

However, since we started with a slice of mutable elements, this limitation is artificial and unfortunate. inout solves this mutability problem between parameters and return values. It is specified on both the parameter and the return type and carries the mutability of the former to the latter:

inout(int)[] inner(inout(int)[] slice)
{
    // ...
    return slice;
}

With that change, the same function can now be called with const, immutable, and mutable slices:

    {
        int[] numbers = [ 5, 6, 7, 8, 9 ];
        // The return type is a slice of mutable elements
        int[] middle = inner(numbers);
        middle[] *= 10;
        writeln(middle);
    }

    {
        immutable int[] numbers = [ 10, 11, 12 ];
        // The return type is a slice of immutable elements
        immutable int[] middle = inner(numbers);
        writeln(middle);
    }

    {
        const int[] numbers = [ 13, 14, 15, 16 ];
        // The return type is a slice of const elements
        const int[] middle = inner(numbers);
        writeln(middle);
    }
lazy

It is natural to expect that arguments are evaluated before entering functions that use those arguments. For example, the function add() below is called with the return values of two other functions:

    result = add(anAmount(), anotherAmount());

In order for add() to be called, first anAmount() and anotherAmount() must be called. Otherwise, the values that add() needs would not be available.

Evaluating arguments before calling a function is called eager evaluation.

However, depending on certain conditions, some parameters may not get a chance to be used in the function at all. In such cases, evaluating the arguments eagerly would be wasteful.

A classic example of this situation is a logging function that outputs a message only if the importance of the message is above a certain configuration setting:

enum Level { low, medium, high }

void log(Level level, string message)
{
    if (level >= interestedLevel) {
        writefln("%s", message);
    }
}

For example, if the user is interested only in the messages that are Level.high, a message with Level.medium would not be printed. However, the argument would still be evaluated before calling the function. For example, the entire format() expression below including the getConnectionState() call that it makes would be wasted if the message is never printed:

    if (failedToConnect) {
        log(Level.medium,
            format("Failure. The connection state is '%s'.",
                   getConnectionState()));
    }

The lazy keyword specifies that an expression that is passed as a parameter will be evaluated only if and when needed:

void log(Level level, lazy string message)
{
   // ... the body of the function is the same as before ...
}

This time, the expression would be evaluated only if the message parameter is used.

One thing to be careful about is that a lazy parameter is evaluated every time that parameter is used in the function.

For example, because the lazy parameter of the following function is used three times in the function, the expression that provides its value is evaluated three times:

import std.stdio;

int valueOfArgument()
{
    writeln("Calculating...");
    return 1;
}

void functionWithLazyParameter(lazy int value)
{
    int result = value + value + value;
    writeln(result);
}

void main()
{
    functionWithLazyParameter(valueOfArgument());
}

The output:

Calculating
Calculating
Calculating
3
scope

This keyword specifies that a parameter will not be used beyond the scope of the function:

int[] globalSlice;

int[] foo(scope int[] parameter)
{
    globalSlice = parameter;    // ← compilation ERROR
    return parameter;           // ← compilation ERROR
}

void main()
{
    int[] slice = [ 10, 20 ];
    int[] result = foo(slice);
}

That function violates the promise of scope in two places: It assigns the parameter to a global variable, and it returns it. Both of those would make it possible for the parameter to be accessed after the function finishes.

(Note: dmd 2.068, the latest compiler used to compile the examples in this chapter, did not yet support the scope keyword.)

shared

This keyword requires that the parameter is shareable between threads of execution:

void foo(shared int[] i)
{
    // ...
}

void main()
{
    int[] numbers = [ 10, 20 ];
    foo(numbers);    // ← compilation ERROR
}

The program above cannot be compiled because the argument is not shared. The following is the necessary change to make it compile:

    shared int[] numbers = [ 10, 20 ];
    foo(numbers);    // now compiles

We will see the shared keyword later in the Data Sharing Concurrency chapter.

return

Sometimes it is useful for a function to return one of its ref parameters directly. For example, the following pick() function picks and returns one of its parameters randomly so that the caller can mutate the lucky one directly:

import std.stdio;
import std.random;

ref int pick(ref int lhs, ref int rhs)
{
    return uniform(0, 2) ? lhs : rhs;
}

void main()
{
    int a;
    int b;

    pick(a, b) = 42;

    writefln("a: %s, b: %s", a, b);
}

As a result, either a or b inside main() is assigned the value 42:

a: 42, b: 0
a: 0, b: 42

Unfortunately, one of the arguments of pick() may have a shorter lifetime than the returned reference. For example, the following foo() function calls pick() with two local variables, effectively itself returning a reference to one of them:

import std.random;

ref int pick(ref int lhs, ref int rhs)
{
    return uniform(0, 2) ? lhs : rhs;
}

ref int foo()
{
    int a;
    int b;

    return pick(a, b);    // ← BUG: returning invalid reference
}

void main()
{
    foo() = 42;           // ← BUG: writing to invalid memory
}

Since the lifetimes of both a and b end upon leaving foo(), the assignment in main() cannot be made to a valid variable. This results in undefined behavior.

The term undefined behavior describes situations where the behavior of the program is not defined by the programming language specification. Nothing can be said about the behavior of a program that contains undefined behavior. (In practice though, for the program above, the value 42 would most likely be written to a memory location that used to be occupied by either a or b, potentially currently a part of an unrelated variable, effectively corrupting the value of that unrelated variable.)

The return keyword can be applied to a parameter to prevent such bugs. It specifies that a parameter must be a reference to a variable with a longer lifetime than the returned reference:

import std.random;

ref int pick(return ref int lhs, return ref int rhs)
{
    return uniform(0, 2) ? lhs : rhs;
}

ref int foo()
{
    int a;
    int b;

    return pick(a, b);    // ← compilation ERROR
}

void main()
{
    foo() = 42;
}

This time the compiler sees that the arguments to pick() have a shorter lifetime than the reference that foo() is attempting to return:

Error: escaping reference to local variable a
Error: escaping reference to local variable b

This feature is called sealed references.

Note: Although it is conceivable that the compiler could inspect pick() and detect the bug even without the return keyword, it cannot do so in general because the bodies of some functions may not be available to the compiler during every compilation.

Summary
Exercise

The following program is trying to swap the values of two arguments:

import std.stdio;

void swap(int first, int second)
{
    int temp = first;
    first = second;
    second = temp;
}

void main()
{
    int a = 1;
    int b = 2;

    swap(a, b);

    writeln(a, ' ', b);
}

However, the program does not have any effect on a or b:

1 2          ← not swapped

Fix the function so that the values of a and b are swapped.