D Programming Language Tutorial
Ali Çehreli



İngilizce Kaynaklar

Diğer



Function Parameters

This chapter covers different ways of defining function parameters.

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

Additionally, we have covered the const and immutable keywords in the previous chapter.

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));
Most parameters are copied

In the code above, the two variables are passed as arguments to weightedAverage() and 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
Objects of reference types are not copied

Elements of slices and associative arrays, and class objects are not copied when passed as parameters. Such variables are passed to functions as references. In effect, the parameter becomes a reference to the actual object; modifications made through the reference modifies the actual object.

Being slices, strings are passed as references 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
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 are a facility that produces values and can have side effects. The in keyword specifies that a parameter is going be used only as an input data by this facility. Such parameters cannot be modified by the function:

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 have seen that the functions return the values that 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 actually 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 effect the actual variable that has been passed to the function. In a sense, the assigned value goes out of the function through out parameters.

Let's have a look at a function that divides two numbers and produces both the quotient and the remainder. The return value can be used for the quotient and the remainder can be returned through an 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 automatically assigned to the .init value of their types:

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 may be better alternatives to out parameters.

const

As we have seen in the previous chapter, 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 the function. const also makes functions more useful by allowing mutable, const and immutable variables to be passed as parameters:

import std.stdio;

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

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

As we have seen in the previous chapter, immutable makes functions require that certain arguments must consist of immutable elements. Because of such a requirement, the following function can only be called with immutable strings (e.g. with 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 brings a requirement on the parameter, immutable parameters should be used only when really necessary. Because it is more welcoming, const is more useful.

ref

This keyword allows passing a parameter by reference even though it would normally be passed as a copy.

For the reduceEnergy() function above to be able to modify the actual variable that is passed as its argument, it must take the 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 actual variable that is passed to the function 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 actual variables. The function parameter energy above is an alias of the variable energy in main().

Similar to out parameters, ref parameters allow functions make side effects as well. In fact, reduceEnergy() does not return a value; it only makes a side effect through its only 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, to write correctly, and to maintain.

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

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.

inout

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

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

To see how inout helps in programs, let's look at a function that returns a slice having one less element from both the front and the back of the original slice:

import std.stdio;

int[] trimmed(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(trimmed(numbers));
}

The output:

[6, 7, 8]

According to the previous chapter, in order for the function to be more useful, its parameter should be const(int)[] because the parameter is 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 argument.)

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

int[] trimmed(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 also as const(int)[] may be the solution:

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

Although the code can now be compiled, it brings a limitation: Even if the function is called with a slice of mutable elements, the returned slice ends up consisting of const elements. To see how limiting this would be, let's look at the following code that is trying to modify the elements of a slice other than the ones at the front and at the back:

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

It would be expected that the returned slice of type const(int)[] cannot be assigned to a slice of type int[]:

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

However, since the original slice is of mutable elements to begin with, this limitation can be seen as artificial and unfortunate. inout solves this mutability problem about parameters and return values. It is specified both on the parameter and on the return type and carries the mutability of the former to the latter:

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

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

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

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

    {
        const int[] numbers = [ 13, 14, 15, 16 ];
        // The return type is slice of const elements
        const int[] middle = trimmed(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 eager.

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

Let's look at such a program that uses one of its parameters only when needed. The following function tries to get the required number of eggs first from the refrigerator. When there is sufficient number of eggs in there, it doesn't need to know the number of eggs that the neighbors have:

void makeOmelet(in int requiredEggs,
                in int eggsInFridge,
                in int eggsAtNeighbors)
{
    writefln("Need to make a %s-egg omelet", requiredEggs);

    if (requiredEggs <= eggsInFridge) {
        writeln("Take all of the eggs from the fridge");

    } else if (requiredEggs <= (eggsInFridge + eggsAtNeighbors)) {
        writefln("Take %s eggs from the fridge"
                 " and %s eggs from the neighbors",
                 eggsInFridge, requiredEggs - eggsInFridge);

    } else {
        writefln("Cannot make a %s-omelet", requiredEggs);
    }
}

Additionally, let's assume that there is a function that calculates and returns the total number of eggs available at the neighbors. For demonstration purposes, the function prints some information as well:

int countEggs(in int[string] availableEggs)
{
    int result;

    foreach (neighbor, count; availableEggs) {
        writeln(neighbor, ": ", count, " eggs");
        result += count;
    }

    writefln("A total of %s eggs available at the neighbors",
             result);

    return result;
}

That function iterates over the elements of an associative array and adds up all of the egg counts.

The makeOmelet() function can be called with the return value of countEggs() as in the following program:

import std.stdio;

void main()
{
    int[string] atNeigbors = [ "Jane":5, "Jim":3, "Bill":7 ];

    makeOmelet(2, 5, countEggs(atNeigbors));
}

As seen in the output of the program, first countEggs() function is executed and then makeOmelet() is called:

Jane: 5 eggs     
Bill: 7 eggs     ⎬ counting the eggs at the neighbors
Jim: 3 eggs      
A total of 15 eggs available at the neighbors
Need to make a 2-egg omelet
Take all of the eggs from the fridge

Although it is possible to make the two-egg omelet with only the eggs in the fridge, the eggs at the neighbors have already been counted eagerly.

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

void makeOmelet(in int requiredEggs,
                in int eggsInFridge,
                lazy int eggsAtNeighbors)
{
   // ... the body of the function is the same as before ...
}

As seen in the new output, when the number of eggs in the fridge satisfy the required eggs, the counting of the eggs at the neighbors does not happen anymore:

Need to make a 2-egg omelet
Take all of the eggs from the fridge

That count would still happen if needed. For example, let's take a look at a case where the required eggs are more than the eggs in the fridge:

    makeOmelet(9, 5, countEggs(atNeigbors));

This time the total number of eggs at the neighbors is really needed:

Need to make a 9-egg omelet
Jane: 5 eggs
Bill: 7 eggs
Jim: 3 eggs
A total of 15 eggs available at the neighbors
Take 5 eggs from the fridge and 4 eggs from the neighbors

The values of lazy parameters are evaluated every time that they are 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 breakes the promise of scope in two places: It assigns the parameter to a global variable and returns it. Both of those would make it possible for the parameter to be accessed after the function finishes.

(Note: dmd 2.066, the compiler that was used last to compile the examples in this chapter, did not support the scope keyword. )

shared

This keyword requires that the parameter is sharable between threads:

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 program can be compiled with the following change:

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

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

Summary
Exercise

... the solution