D from the ground up

Functions, defaults and ref

Naming a piece of work, letting a caller leave an argument out, changing a value through ref, and letting one name cover two kinds of argument.

Part 6 of 12

Intermediate16 min readBack to Courses

A name for a piece of work

A function declares the type of its answer, its name, and the parameters it needs: int fee(int cents, int rate = 10). The body returns the answer with return, and a function whose job is only to print declares void, the answer that is nothing at all.

Moving a rule into a function gives it one home. When the rule changes, one body changes and every call follows; a rule copied into three places has to be found and changed three times, and the copy that is missed is the one that ships.

Defaults and ref

A parameter may carry a default, written after an equals sign, and then the caller can leave it out. Defaults go after the required parameters so the compiler always knows which argument is which. fee(200) uses the default rate and fee(200, 25) overrides it.

By default an argument is passed by value, so the function works on a copy and the caller never sees a change. Writing ref in front of a parameter passes the variable itself: void bump(ref int score) can change the caller value, and that is a promise worth keeping visible in the signature.

A default is part of the declaration, not of the call: it is resolved and checked where the function is written, and the caller who leaves the argument out gets that value. That is why a default can name a module-level constant but cannot read a local variable belonging to the caller.

One name, two argument types

Two functions may share a name when their parameters differ, which is overloading. tag(string) and tag(int) sit beside each other, and the compiler picks one from the argument at the call. The call sites stay short, and each body says what that kind of value needs.

A name should mean one operation. If the two overloads do unrelated work, the shared name hides the difference; if they do the same thing to different types, the shared name is the right signal, and a template is often the way to write it once — the subject of a later chapter.

Overload resolution looks at the argument types, not at the names or the return type. Two functions that differ only in what they return are the same signature, so the compiler refuses them; the caller could not say which one it meant.

What a call actually does

A call creates a stack frame holding the parameters and the local variables, runs the body, and hands the returned value back before the frame disappears. Parameters without ref are copies placed in that frame, so assigning to one changes the copy and nothing else. That is why a function can reuse a name without disturbing the caller.

A ref parameter is not a copy: it is another name for the caller storage, and the frame holds a reference to it. out is a ref with one extra promise. The parameter is default-initialised on entry, so the body can read it from the first line and the caller cannot rely on what was there before.

Recursion and the return path

A function that calls itself is ordinary: each call gets its own frame, and the frames unwind when the base case returns. factorial(5) builds five frames and multiplies on the way back. The base case is what stops it; a missing or unreachable base case is a stack overflow rather than a wrong answer.

The compiler checks that a function with a return type returns on every path it can see. That is why an if with no final else and no return after it is an error even when the missing branch feels impossible. Writing the last return explicitly satisfies the check and documents the fallback.

A default parameter

import std.stdio;

int area(int w, int h = 2) {
    return w * h;
}

void main() {
    writeln(area(5), " ", area(5, 3));
}

One default, one ref, two overloads

Run it and match each line to its call. Then call fee with a third argument of your own, and add an overload of tag that takes a double and prints the value with one decimal.

Output
Click "Run" to execute your code

A ref parameter cannot take a literal: bump(40) will not compile, because there is no variable to change. Keep the value in a variable first when you want to pass it by reference.

A factorial and two out parameters

Run it and trace the five calls that `factorial(5)` makes. Then call `factorial(0)` and check the base case, and pass an array of your own to `minMax`.

Output
Click "Run" to execute your code

Three mistakes: changing a by-value parameter and expecting the caller to see it; passing a literal to a ref parameter, which cannot compile because there is nothing to change; and forgetting a return on one branch of a non-void function, which the compiler reports rather than leaving the value undefined.

Try these next

  • Call factorial(0) and factorial(1) and check that the base case covers both.
  • Write a second out parameter that reports how many values were above the average.
  • Turn factorial into an iterative function and compare the two versions line by line.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does a default value let a caller do?