Return to home

Introduction

This lesson we will talk about functions and how we can use D to read input from the user. We also will explore some of the things you need to do when reading input from the user, and how to run a input event loop.

Before we start, I wanted to mention that coding requires typing, and typing as a skill will be very valuable in other computer-related tasks as well, not just programming! If you do not yet know how to "touch-type", I recommend using a typing tutor, or typing game, to hone your skills. To that end, I recommend the typing.com web site. One thing that is quite enjoyable for practice is the typing games page on that site.

Functions

Functions are a way to declare a list of instructions that you want to run based on inputs. Like variables, we assign this set of instructions to an identifier so you can refer to it later and the computer knows what we mean. Having this mechanism makes code much simpler to write, and easier to understand -- you can look at a function and understand how it is doing what it does, and we can look at the code that calls it and understand what it is doing by calling the function.

If we use a metaphor, imagine that I ask someone to do 5 jumping jacks. That person performs the requested activity. I just called a function! I did not have to tell the person, "first jump and spread your legs out. At the same time, move your arms up into the air" and so on. Instead I just said "do 5 jumping jacks".

If you imagine the function in this case would be called doJumpingJacks and it takes as input the person to perform the jumping jacks, and the number of jumping jacks to do. Inside the person's brain, they already have the doJumpingJacks code defined, so we don't have to specify all the details. The communication between us is simpler, and the understanding of what will happen overall is easier to grasp.

Let's look at a function that adds 2 integers together (note, this function has almost no use, this is just to examine the syntax of functions):

int add(int x, int y) {
    return x + y;
}

In D, a function is defined similarly to a variable. It is defined with a type followed by an identifier. The difference here is the parentheses. If you use parentheses, then the identifier is a function, without parentheses it is a variable. Inside the parentheses would go the input parameters for the function. If there are no parameters (like for example, the main function), then nothing goes between the parentheses. Function parameters are defined exactly the same as variables. You specify the type of the parameter and an identifier for the parameter to use within the function.

After the closing paranthesis, the next thing you type is the opening curly brace {. This denotes a block statement which will contain all the statements of the function. Similar to an if statement or while loop, everything between the curly braces will be considered the body of the function, or the instructions the computer must run when calling that function.

In our example, the add function takes 2 parameters, x and y, with type int. Inside the function, we add the two integers together, and return the result. The return statement is telling the computer that the value calculated should be made the output of the function. Notice the type specified for the function, int. This is what type the return value needs to be. In this case, when you add two ints together, you get an int as a result, so the types match!

Calling a function

How do we use this function? Using a function is known as calling the function. This means you tell the computer to use the function denoted by the identifier, with the arguments given. The arguments must match the parameters of the function. Calling a function has a syntax similar to defining a function. When you define a function, you include the type, but when you call a function, you omit the type of the function, and the types of the arguments. Let's call the add function from main:

import std.stdio;

int add(int x, int y) {
    return x + y;
}

void main() {
    int result = add(1, 2);
    writeln(result);
}

In the above code, we called the function using add(1, 2). This tells the computer to call the add function with 2 arguments, 1 and 2. Inside the function the parameter x will have the value 1, and the parameter y will have the value 2, only for this use of the function.

The function will add those two numbers together, and the result of 3 is returned. What happens in the main function? The function call to add is replaced with the value returned from the function. It's the same as saying int result = 3;. So now, the result variable has the value 3, which we can see by using writeln to show it!

The return from a function can be used in any place where that type might be needed. For example, if I wanted to use our add function to add 1 + 2 + 3, I can't call add(1, 2, 3) because it only takes 2 parameters. However, I can substitute one of the parameters with another call to the add function, because add accepts two ints and it returns an int. Therefore, we can call add(add(1, 2), 3).

This may look odd, but remember that every call to a function is independent, and is replaced with its result before being used. So first, the compiler calls add(1, 2) and gets back the result 3. It then replaces 3 for the first parameter to the outer add call, resulting in the call add(3, 3).

A function that has a void return type does not return a value. You cannot assign the result of it to a variable. If you wish to return from this function early, you use return; without any value.

I will note here that when you use import what you are mainly doing is telling the compiler to go read that module, and learn all the functions that are in that file. In this way, you can teach the compiler all the things it needs to know to execute your code.

Reading Input

So far, we have had no input from the user. The only input has been from the code itself. But these kinds of programs are not too useful, either they do the same thing every time, or in order to get it to change what it does, you need to rebuild the program.

For graphical programs, we have all sorts of input -- from the keyboard, from the mouse, from a game controller, etc. But for a text-based non-graphical program, we have really only 3 forms of input -- command line parameters to the program, files read by the program, and input typed in by the user. For now we will focus only on the input typed in by the user.

There are a few ways to read data from the input stream, but we are going to use a very straightforward function from std.stdio called readln. This function has no parameters, and returns as a string the next line of text read from what the user types.

Preparing the input

Here is a program that reads two lines from the user, and prints them out:

import std.stdio;

void main() {
    string line = readln();
    writeln("You typed: ", line);
    line = readln();
    writeln("You typed: ", line);
}

If you enter this program into VSCode, and run it, you will in the terminal window that it just sits there with cursor. It is waiting for you to type a line. The next thing you will notice is that it prints the lines you type, but it also leaves a blank space between the lines. Why is that?

The reason is that the readln is also giving you the control character which corresponds to "Enter", or "Return" on your keyboard, called a newline! When you use writeln, it is adding an additional newline character to the output, making it skip an additional line.

We can remove this extra "whitespace" by calling yet another function, strip. This strips any whitespace (spaces, tabs, newlines, etc.) surrounding a string, so all you have left is the text typed. Any spaces between non-space characters are left alone. This function is defined in the std.string module, so we must import it:

import std.stdio;
import std.string;

void main() {
    string line = strip(readln());
    writeln("You typed: ", line);
    line = strip(readln());
    writeln("You typed: ", line);
}

Now, the extra lines are not printed, because we stripped them. Note how we took the result of the function readln, which is a string, and passed it in as the first argument to strip, which takes a string and returns a string. Recall that calling functions and passing the results to other functions can be done as long as the types match.

Prompting and looping

This program is simple, it takes input, but it doesn't do anything interesting. It just regurgitates what the user has said, and only does that twice. What if we wanted to do it until the user types something else? What if we wanted to read in numbers instead of text, and do something with those numbers?

Next we are going to change the program into an input loop. This is a loop which runs until the user tells it to stop, processing the input each time through the loop. Let's define a simple processor which accepts numbers, and adds those numbers each time through the loop. When the user types "exit", then we will exit the loop and the program. Here is that program, but it has some problems.

import std.stdio;
import std.string;

void main()
{
    writeln("This is a number adder. Please type in numbers, and I will add");
    writeln("them up.");
    writeln("type exit when you are done!");
    int sum = 0;
    while(true)
    {
        string input = strip(readln());
        if(input == "exit")
        {
            // exit the loop
            break;
        }

        sum += input;
        writeln("Sum: ", sum);
    }
    writeln("Exiting!");
}

The writeln function calls at the start print out instructions to the user, to tell them how to use the program, and helps cue them to the fact that we are waiting for input. But unfortunately, this program has a problem...

Converting Between Types

source/app.d(19,13): Error: incompatible types for `(sum) += (input)`: `int` and `string`

If you compile this code, you will note that it fails. It fails because you cannot add a string (input) to an integer (sum). How do we convert the string into an int?

In D, conversion between types is done by a module called std.conv. Inside that module is a special function named to. This function has a special parameter that is passed in a weird way. We aren't going to explore exactly how this works because it is an advanced topic, but you don't need to know that to use it. Just know that the syntax to!int(anything) is going to tell the function to try to convert the parameter to an int. The exclamation point argument says which type you want to try converting to. The parameter is the value you want to convert. Converting from an int to a string would be done via to!string(someInt).

Now, let's update the program:

import std.stdio;
import std.string;
import std.conv;

void main()
{
    writeln("This is a number adder. Please type in numbers, and I will add");
    writeln("them up.");
    writeln("type exit when you are done!");
    int sum = 0;
    while(true)
    {
        string input = strip(readln());
        if(input == "exit")
        {
            // exit the loop
            break;
        }

        int n = to!int(input);
        sum += n;
        writeln("Sum: ", sum);
    }
    writeln("Exiting!");
}

Note the while(true) in the code. Normally, this would be an infinite loop. However, the break statement is how we get out of it. The break statement means "exit the loop". This allows you to check for loop stopping conditions outside the normal condition check, and quit the loop early.

If we didn't use this mechanism, we would have to call all the code that reads the input twice, once before the loop start (to get the value to check), and once inside the loop.

Variable Scoping

I wanted to mention this, even though it's a bit advanced, because this is a common problem that new programmers make -- you need to make sure you define your variables in the correct places, and you will get weird errors if you don't!

Notice how we defined int sum = 0; before the loop. A variable in D lives only as long as the scope in which it is declared. After that, the compiler doesn't know about it, and it doesn't exist. In this case, we need sum to stay alive and retain its value the whole time the loop is executing. If we defined it inside the curly braces of the loop, the code would compile, but sum would reset to 0 every time through the loop, because it is a new variable for that one loop execution! The same goes for functions or if statements. Things declared inside the braces can't be used outside the braces, and they lose their value if you exit and enter that scope again.

Exercise

For the exercise, let's change the stopping condition to either reading "exit" or the value 0. And we will write a function to read input and return an integer. The function should read the next line from the user, and check for "exit", and if it's that, it should return 0. Otherwise, it should convert to an integer and return. In the following code, you do not need to touch any code inside the main function.

Note that you can return early from a function and it will immediately stop executing that function and return the value.

import std.stdio;
import std.string;
import std.conv;

int readNumber()
{
    // write your function here
}

void main()
{
    writeln("This is a number adder. Please type in numbers, and I will add");
    writeln("them up.");
    writeln("type exit or 0 when you are done!");
    int sum = 0;
    while(true)
    {
        int n = readNumber();
        if(n == 0)
        {
            // exit the loop
            break;
        }

        sum += n;
        writeln("Sum: ", sum);
    }
    writeln("Exiting!");
}

Try working it out for yourself in VSCode, or on run.dlang.io. If you can't figure it out, you can

Return to home

©2023 Steven Schveighoffer