
-----------------------------------
wtd
Wed Jul 20, 2005 7:41 pm

D: A Newbie-Oriented Tutorial
-----------------------------------
A.  Guidelines


Do not advance beyond any section unless you completely understand it.
Use meaningful names for things.  Code should be self-documenting as much as possible.


B.  It's been my observation...

Of late I've noticed a number of people interested in learning C++.  This is probably most commonly attributed to a desire to have a running head start when taking a programming class which uses C++.

This plan fails, in my estimation, on a few critical points.


Learning by yourself, without the benefit of considerable experience or a driving passion for the subject, is tedious, and comes with very little support.

In a classroom environment you'll have teachers and fellow students to assist you.  Further, these students will be doing exactly what you are, so there will be far fewer communication hurdles than there are in soliciting help from a forum on the web.
C++ is complex, and that complexity is harnessed for even the simplest tasks.  Sometimes this makes those tasks easierin the long-run, but it means that you must tackle complex concepts to understand even a simple "Hello world" program.

I've also noted that many students seem quite preoccupied with syntax, and are often therefore resistant to languages myself and others can suggest which do not bear a syntactic similarity to the C-like syntax family (to which C++ belongs). 

C.  So, what to do?

Certainly Java is an option.  Java, however, lacks a lot of the functionality present in C++, uses different syntax in many cases, and behaves rather differently in general.

Java also imposes a rather high mental overhead on programmers trying to learn it.  Object-oriented programming can't be left for later, for instance, since knowledge of it is essential for even the simplest programs.

C would fit, except that it's just too low-level.  

There is another good language that fits for the task, though it's less well known.  The D programming language designed by Digital Mars is a good alternative that may very well be easier to learn than C++, while giving a familiarity with the general "feel" of C-like syntax.

D.  Getting a Compiler

A free D compiler is available from:

prompt> dmd myprogram

Or:

prompt> dmd myprogram.d

This will generate an executable named "myprogram" on Linux or "myprogram.exe" on Windows, unless there is an error, which it will show you information about.

Compiling with the "-w" flag will show any warnings.  These are not errors which will prevent compilation, but they are potential problems.  Compiling with this flag is a matter of:

prompt> dmd myprogram.d -w

1.  Everything has a beginning

Let's dive right in by looking at the simplest possible D program.  

void main()
{

} 

Every program has a "main" function which serves as the entry point.  The program begins execution at the beginning of the function, and when execution reaches the end of the main function, the program exits.

2.  Functions

So, now that I've demonstrated a function, I should probably explain what a function is.

Functions are the means by which all executable code in a program is organized.  Data is organized in other ways, but for now we'll deal with executable code and simple data.

A function has four major components.


A name.  Every function has some name.  In the case of the main function, that name is "main".
Every function has a return type.  This indicates the type of data the function returns to the program.  In the main function we've seen, that return type is "void".  This means that the function return nothing to the program.

In the case of the main function, it should be noted that it can also return an integer, and in this case, that integer is returned to the operating system as an indicator of whether or not the program succeeded.  If we return nothing, the OS assumes the program finished successfully.
Every function has a body of code that is executed when the function is called.  This code comes between the curly braces.
Ever function has a list of parameters.  Parameters are values passed to the function which can then be used within that function to alter its behavior.

These parameters each have a type and a name, fall between the parentheses in a function declaration, and as in the main function we've seen, there may in fact be no parameters for a function.


Think of a function as the classic "black box".  It has inputs in the form of parameters, and its return value is the output.  What happens inside the function is impossible and unnecessary to know.

All that matters is that the function does what it says it will.  

Let's look at a simple function of our own devising.  This function will take an integer as a parameter and square it, returning the result.

First we write what we can think of as the interface to the function: its name, return type, and parameters.

int square(int number)
{

} 

Now we need to be able to square a number.  The simplest way to accomplish this is by multiplying it by itself.

number * number

At this point all we lack is the ability to return that result out of the function.  For this we naturally se the "return" statement.

int square(int number)
{
   return number * number;
}

A semi-colon ends the statement.

Calling the function is quite simple.  In this case we'll call it from within the main function, and do nothing with its return value, which is a valid course of action, though generally not advised.

void main()
{
   square(42);
}

int square(int number)
{
   return number * number;
}

3.  Statements and Expressions

In the previous section, I referred to "the return statement".  The last of those three words is tremendously important.

A statement is anything in a programming language that does something to the environment, but returns no new value to the program.

To complement this, we have expressions, which do compute and return to the program new values.  The program we created in the previous section contains two expressions.

number * number

Is an expression.  It multiples "number" by itself and returns the result to the program.  In the process it does not change the number parameter.

square(42)

Is also an expression.  It takes the value 42, computes a new value, and returns that value to the program, which promptly ignores it, in this case.

The difference between statements and expressions is a fundamental programming concept.

4.  Variables

As mentioned previously, the "square" function, when called forms an expression which returns a new value to the program.  However, we ignore that return value.   

There are two things we can do with that value, aside from ignoring it.  We can either immediately use the value by passing it to another function, or we can store it in a variable.

Variables are essentially pieces of memory in which we can store values.  Every variable has a name and a type of data which it can store.

All variables must be declared prior to use.   This is accomplished by preceding the name with the type of the variable.  To store the result in our previous program we might declare a variable as follows.

void main()
{
   int result;

   square(42);
}

int square(int number)
{
   return number * number;
}

We can then easily assign a value to the variable, as long as the type of the value matches the type of the variable.

void main()
{
   int result;

   result = square(42);
}

int square(int number)
{
   return number * number;
}

Fortunately we can both declare the variable and give it an initial value in one statement.

void main()
{
   int result = square(42);
}

int square(int number)
{
   return number * number;
}

We can, of course, pass variables to functions.

void main()
{
   int initialNumber = 42;
   int numberSquared = square(initialNumber);
}

int square(int number)
{
   return number * number;
}

5.  Simple Output

Thus far the examples program haven't actually done anything apparent to the user.  Let's change that by incorporating some simple output.

import std.stdio;

void main()
{
   puts("Hello world!");
}

Here we start by importing the std.stdio module.  This module contains, among other things, several useful functions for dealing with output.  By importing it, we make these functions available in our program.

One such function is "puts", which is an abbreviation of "put string".  A string is simply a series of individual characters.

The puts function simply prints a string to the standard output, and then moves to a new line.  An alternative and popular function to do the same is "writefln".

import std.stdio;

void main()
{
   writefln("Hello world!");
}

The "ln" in the function name indicates the fact that it skips to a new line.  The "f" is an abbreviation for "format".  Fomrat strings make it possible to output other data using formatting characters.

Let's revisit our previous program:

void main()
{
   int initialNumber = 42;
   int numberSquared = square(initialNumber);
}

int square(int number)
{
   return number * number;
}

And let's add an output statement to that so that we see:

42 squared is 1764

void main()
{
   int initialNumber = 42;
   int numberSquared = square(initialNumber);

   writefln("%d squared is %d",
            initialNumber,
            numberSquared);
}

int square(int number)
{
   return number * number;
}

The "%d" indicates an integer.  Other popular options are "%f" for a floating point number, "%c" for a character, and "%s" for a string.

Many other options are available, but will not be covered here yet.

6.  Arrays

In the previous section I talked about strings, and said that they were a "series of individual characters".  This is true, but more accurately a string is an array of character.

An array is an immensely useful way of organizing data.  It stores several values of a single type in a block of memory.  It also allows for random access to its contents.  In other words, it's as easy and quick to access the five-hundredth element as it is to access the first.

Creating an array is as simple as appending "int[] intArray;

An array is indexed by numbers starting with zero.  To put a value into the array in the first position, I would:

intArray[0] = 42;

Inserting a value at the five-hundredth space would then be:

intArray[499] = 73;

You should note that I never specified how long the array should be.  Instead I simply assigned values to various positions in the array and it just worked.

This is an example of a dynamic array.  It will grow to whatever size is required.  I can determine the size of an array at any time by accessing the array's length property.  The length property can also have a value assigned to it to resize the array.

import std.stdio;

void main()
{
   int[] intArray;

   intArray[0] = 42;
   intArray[1] = 54;
   intArray[2] = 91;

   writefln("The length of intArray is %d.",
            intArray.length);
}

If, however, I wish to create an array of a fixed size, I may do so.

int[3] fixedSizeIntArray;

Now, if I attempt to insert a value into the array outside of the bounds I set, an error will occur.  The following, for instance, will not work.

fixedSizeIntArray[3] = 56;

Again, the length of a fixed size array can be determined by the length property.  In this case, however, the length is read-only, and cannot be changed.

Fixed-size arrays can be conveniently initialized.  To create a fixed-size array with 4 elements:

static int[] fixedSizeIntArray = [56, 67, 42, 19];

Strings are simply an array of characters.  Typically we deal with strings as dynamic arrays, but we can deal with strings as fixed size arrays.

Creating and initializing a simple string is as easy as:

char[] myString = "Hello world";

We can then use that string in functions.

import std.stdio;

void main()
{
   char[] myString = "Hello world";
   
   writefln(myString);
}

It's through the use of strings that we can see many of the interesting operations that are available for arrays.

For example, concatenating two arrays together into one array:

char[] helloString = "Hello";
char[] worldString = "world";
char[] helloWorldString = 
   helloString ~ ' ' ~ worldString;

Accessing a portion of a string, or any other type of array, is done using an array "slice".  To grab the "llo" from "Hello":

char[] subString = helloString[2 .. 5];

7.  Associative Arrays

In the previous sections, I discussed dynamic arrays which resize as necessary.  Thus I can have something like:

int[] intArray;

intArray[0] = 42;
intArray[1001] = 98;

The potential problem with this is that the array created is not "sparse".  In order to create an array with an index of 1001, it creates everything upto 1001 as well.

While dynamic arrays are quite valuable, for situations where the indexes are relatively small and sequential.  But if the indexes are large and sparse, a more memory-efficient solution is to use an associative array.

An associative array, as the name indicates, associates one type of value with another.  Conveniently, it can use just about any type of data as the key.  Consider if we wish to have an array of strings, indexed by double-precision floating point numbers.

We would declare that array like so:

char[][double] myAssociativeArrayOfStrings;

Then we can easily insert values into the array.

myAssociativeArrayOfStrings[34.5] = "Hello";

And easily access the values.

writefln(myAssociativeArrayOfStrings[34.5]);

Perhaps a less contrived example of the usefulness of this is keeping track of scores for a set of players in a game.

int[char[]] scoresByPlayer;

scoresByPlayer["Chris"] = 43981;
scoresByPlayer["John"]  = 67215;

Of course, at this point you're probably wondering how we can even begin to know what keys are available at any given time.  As a solution to this problem we have the keys property which returns a dynamic array of keys for the associative array.

As with other arrays, associative arrays also have a length property which will indicate the number of entries.

8.  Iterating

Now that we've seen arrays which can contain many values, it should be fairly clear that it would be tedious to have to deal with each of those values separately, especially since most of the code is going to be repeated.

Fortunately there are a couple of ways to iterate over the contents of an array.

The classic "for" loop uses a counter variable.  This variable has an initial value, a test, and an update.  As long as the test holds true, the loop continues.  The loop also has a body.  This body code gets run on each loop.  After it runs, the update code is run.

Let's start with an array of integers representing a set of scores in a game.

static int[] scores = [5621, 9845, 1274];

We then write out the basic shell of a for loop.

for ( ; ; )
{

}

Let's start out with a counter with an initial value of zero, since arrays are indexed starting with zero.

for (int scoreIndex = 0; ; )
{

}

We then need a test.  We know that the indexes of an array will always be one less than the length of the array.

for (int scoreIndex = 0; scoreIndex < scores.length; )
{

}

And lastly we need an update.  In this case, we simple increment the counter.

for (int scoreIndex = 0; scoreIndex < scores.length; scoreIndex++)
{

}

Now we just need some code to run each time.

for (int scoreIndex = 0; scoreIndex < scores.length; scoreIndex++)
{
   writefln("Score #%d is %d",
            scoreIndex,
            scores[scoreIndex]);
}

This allows us some considerable freedom in how we iterate over arrays.  We could access every other score by changing the update code.

for (int scoreIndex = 0; scoreIndex < scores.length; scoreIndex += 2)
{
   writefln("Score #%d is %d",
            scoreIndex,
            scores[scoreIndex]);
}

However, probably the most common way of iterating over an array involves accessing each element in order with no regard for the index.  In this case, we're provided with a shortcut.

The "foreach" loop can be used to more easily access elements of an array.

foreach (int score; scores)
{
   writefln("Score is %d", score);
}

Of course, we could still maintain an index.

int scoreIndex = 0;

foreach (int score; scores)
{
   writefln("Score #%d is %d", 
            scoreIndex,
            score);
   scoreIndex++;
}

The "for" loop style is often used when we want to not only access, but also modify values in an array.  Consider if we wish to give ourselves a little ego boost and add 100 points to all of our scores.

for (int scoreIndex = 0; scoreIndex < scores.length; scoreIndex++)
{
   scores[scoreIndex] += 100;
}

We can achieve this more easily with the "foreach" loop, though.  We need only specify that the element in the array which we're accessing is "inout", or available both to access and to change.  It can go "in", and come "out" of the loop with a new value.

foreach (inout int score; scores)
{
   score += 100;
}

9.  If Statements

At some point you're certain to wonder how we can choose one course of action or another.  To accomplish this we use the "if" statement.

This involves a test, and if the test is true, a block of code is executed.  If it's false, however, then an optional "else" block is executed.

Let's tie this into the example in the previous section and offer commentary on a set of grades.

import std.stdio;

void main()
{
   static int[] scores = [5621, 9845, 1274];

   foreach (int score; scores)
   {
      if (score < 4000)
      {
         writefln("%d?  What a pathetic score.",
                  score);
      }
      else
      {
         writefln("%d...  Not bad.",
                  score);
      }
   }
}

Computers can be so cruel.

Of course, we can be a bit more specific than that.

if (score < 4000)
{
   writefln("%d?  What a pathetic score.",
            score);
}
else if (score < 7500)
{
   writefln("%d...  Not bad.",
            score);
}
else
{
   writefln("%d!  Whoa... I'm not worthy.",
            score);
}

10.  On Style and Brevity

In the previous section, I used curly braces extensively to indicate blocks.  However, if there is only a single statement, the curly braces are optional.  

The previous example could be rewritten as:

import std.stdio;

void main()
{
   static int[] scores = [5621, 9845, 1274];

   foreach (int score; scores)
      if (score < 4000)
         writefln("%d?  What a pathetic score.", score);
      else if (score < 7500)
         writefln("%d...  Not bad.", score);
      else
         writefln("%d!  Whoa... I'm not worthy.", score);
}

This style should be used very sparingly.

11.  Arrays and "main" and More on Looping

The main function we've seen thus far has no parameters.  This is acceptable, but there is another acceptable form.

void main(char[][] commandLineArguments)
{

}

This specifies an array of strings as the sole parameter to main.  This array will contain any arguments passed to the program at the command-line.  The first argument is always the name of the program itself, so the arguments really only begin with the second element of this array.

Let's look at a simple "Hello world" program.

import std.stdio;

void main()
{
   writefln("Hello world");
}

Now, let's introduce a function so that we can say hello to anyone.

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}

And we'll put that all together to greet someone named "Chris".

import std.stdio;

void main()
{
   sayHelloTo("Chris");
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}

But that's still pretty static.  Let's pass the name of the person to greet into the program via a command-line argument.

import std.stdio;

void main(char[][] commandLineArguments)
{
   sayHelloTo(commandLineArguments[1]);
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}

There's a problem, though. What if I didn't pass in an argument?  The commandLineArguments array will only contain one element, and my access of it will be out of bounds.  An error will occur.

I can detect the length of the array, though, and I can do one thing or another with the if statement.  Together I can use these to avoid problems.

import std.stdio;

void main(char[][] commandLineArguments)
{
   if (commandLineArguments.length > 1)
      sayHelloTo(commandLineArguments[1]);
   else
      sayHelloTo("world");
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}

And what if I wanted to greet several names?  Certainly I can pass several arguments to the program at the command-line. 

Well, in this case, I want to use a loop.  But I want to avoid the first element.  There are a few ways of accomplishing this.

I could loop over every element in the array, and maintain a counter.  When the counter is zero, I could use the "continue" statement to skip to the next iteration immediately, being certain to increment the counter first.

import std.stdio;

void main(char[][] commandLineArguments)
{
   int argumentCounter = 0;

   foreach (char[] argument; commandLineArguments)
   {
      if (argumentCounter == 0)
      {
         argumentCounter++;
         continue;
      }

      sayHelloTo(argument);
      argumentCounter++;
   }
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}

The other option available to me is to simply start iterating at the second element.  The most reasonable way to accomplish this would seem to be array slicing.  As seen with strings, we can access a portion of an array called a "slice".

Again, since we can't count on any arguments being passed in, we should test to see if arguments other than the name of the program are present.  Once we've ascertained that, we can create the slice and iterate over it.

import std.stdio;

void main(char[][] commandLineArguments)
{
   if (commandLineArguments.length > 1)
   {
      foreach (char[] argument; commandLineArguments[1 .. length])
      { 
         sayHelloTo(argument);
      }
   }
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}  

You should note the odd presence of the undeclared "length" variable.  Conveniently, the length variable is always created within a slice to refer to the length of the array being sliced.  Without this convenience, we'd have to write:

commandLineArguments[1 .. commandLineArguments.length]

Let's look at another useful statement within a loop.  The "break" statement allows us to exit out of the loop entirely at an arbitrary point.

For instance, we may wish to greet people until we run into "Mortimer".  Then we'll stop greeting anyone.

import std.stdio;

void main(char[][] commandLineArguments)
{
   if (commandLineArguments.length > 1)
   {
      foreach (char[] argument; commandLineArguments[1 .. length])
      { 
         if (argument == "Mortimer")
         {
            writefln("Mortimer?  Are you kidding?  I'm done with this.");
            break;
         }

         sayHelloTo(argument);
      }
   }
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}

12.  Function Pointers and Aliases

As we've seen already, functions are the means by which all executable code in a program is organized.  In other words, they're among the most important "things" in a program.  

Yet, it doesn't seem we can actually do much with them.  We can call them, and that's about it.

Well, as it happens, we can also pass them around like variables.  This ability comes about due to the fact that a function is really just a place in memory where a certain section of executable code starts.  We can get a "pointer" to this place in memory, store that, and then use it to call the function later on.

In the previous section we said hello to any name passed in as an argument at the command-line.  We defined the sayHelloTo function to accomplish this.  We needn't be limited to just that one function, though.  Let's create a less upbeat function.

void tellOff(char[] nameToTellOff)
{
   writefln("Damn you %s!",
            nameToTellOff);
}

And maybe a slang version:

void greetWithSlang(char[] nameToGreetWithSlang)
{
   writefln("Yo %s.  Sup?",
            nameToGreetWithSlang);
}

Now, we could simply write a program which greets all of the names with all three functions like so:

import std.stdio;

void main(char[][] commandLineArguments)
{
   if (commandLineArguments.length > 1)
   {
      foreach (char[] argument; commandLineArguments[1 .. length])
      { 
         sayHelloTo(argument);
      }

      foreach (char[] argument; commandLineArguments[1 .. length])
      { 
         tellOff(argument);
      }

      foreach (char[] argument; commandLineArguments[1 .. length])
      { 
         greetWithSlang(argument);
      }
   }
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}

void tellOff(char[] nameToTellOff)
{
   writefln("Damn you %s!",
            nameToTellOff);
}

void greetWithSlang(char[] nameToGreetWithSlang)
{
   writefln("Yo %s.  Sup?",
            nameToGreetWithSlang);
} 

However, we should always be looking for ways to streamline our code, and reduce redundancies.  Therefore, as we look at the above program, we notice that the three loops are exactly identical, except for the name of the function being called.

So, we need to identify a generic description of these functions.  We know that they all return void.  We know that they all take one argument: a string.  That's enough info.

void function(char[])

We can now declare a pointer to a function.

void function(char[]) greeterFunction;

Initializing this variable takes advantage of the & operator, which finds the memory address of a variable or function.

void function(char[]) greeterFunction = &sayHelloTo;

As with other data types, we can now create an array of these, and statically initialize it with the three functions we've created thus far.

static void function(char[])[] greeters = 
   [&sayHelloTo, &tellOff, &GreetWithSlang];

At this point, the type is beginning to look sloppy.  We can use a type alias to clear this up.

alias void function(char[]) GreeterFunction;

We can now write:

static GreeterFunction[] greeters = 
   [&sayHelloTo, &tellOff, &GreetWithSlang];

Now, we just need to incorporate this back into the program.  We'll need to iterate over each of the function pointers, and for each of those, iterate over the arguments passed in at the command-line.

import std.stdio;

void main(char[][] commandLineArguments)
{
   alias void function(char[]) GreeterFunction;
   
   static GreeterFunction[] greeters = 
      [&sayHelloTo, &tellOff, &GreetWithSlang];

   if (commandLineArguments.length > 1)
   {
      foreach (GreeterFunction greeter; greeters)
      {
         foreach (char[] argument; commandLineArguments[1 .. length])
         { 
            greeter(argument);
         }
      }
   }
}

void sayHelloTo(char[] nameToGreet)
{
   writefln("Hello, %s!", nameToGreet);
}

void tellOff(char[] nameToTellOff)
{
   writefln("Damn you %s!",
            nameToTellOff);
}

void greetWithSlang(char[] nameToGreetWithSlang)
{
   writefln("Yo %s.  Sup?",
            nameToGreetWithSlang);
}

13.  Labels and Breaking and Continuing in Nested Loops

In the previous section we ended up with a program with nested loops.  This has considerable ramifications, as we'll see.

First, though, we'll try to apply our filter for the name Mortimer.  Previously we had broken out of the loop when we encountered that name.  Let's do the same here.  For brevity's sake, only the main function is shown, since the other functions remain unchanged.

import std.stdio;

void main(char[][] commandLineArguments)
{
   alias void function(char[]) GreeterFunction;
   
   static GreeterFunction[] greeters = 
      [&sayHelloTo, &tellOff, &GreetWithSlang];

   if (commandLineArguments.length > 1)
   {
      foreach (GreeterFunction greeter; greeters)
      {
         foreach (char[] argument; commandLineArguments[1 .. length])
         { 
            if (argument == "Mortimer")
            {
               writefln("Mortimer?  Are you kidding?  I'm done with this.");
               break;
            }

            greeter(argument);
         }
      }
   }
}

When we run this we notice an odd behavior.  The program will run upto "Mortimer", then run the next function upto Mortimer, and then the next function.  

We wanted the program to stop looping entirely when it encountered that name.  The problem lies with the default behavior of break.  It will break out of the innermost loop.

In order to change this behavior we must first give labels to the loops, effectively naming them.

import std.stdio;

void main(char[][] commandLineArguments)
{
   alias void function(char[]) GreeterFunction;
   
   static GreeterFunction[] greeters = 
      [&sayHelloTo, &tellOff, &GreetWithSlang];

   if (commandLineArguments.length > 1)
   {
      functionLoop: 
      foreach (GreeterFunction greeter; greeters)
      {
         argumentLoop:
         foreach (char[] argument; commandLineArguments[1 .. length])
         { 
            if (argument == "Mortimer")
            {
               writefln("Mortimer?  Are you kidding?  I'm done with this.");
               break;
            }

            greeter(argument);
         }
      }
   }
}

The default behavior of the break statement remains the same.  Its behavior can be modified, though, by telling it which loop to break out of.

import std.stdio;

void main(char[][] commandLineArguments)
{
   alias void function(char[]) GreeterFunction;
   
   static GreeterFunction[] greeters = 
      [&sayHelloTo, &tellOff, &GreetWithSlang];

   if (commandLineArguments.length > 1)
   {
      functionLoop: 
      foreach (GreeterFunction greeter; greeters)
      {
         argumentLoop:
         foreach (char[] argument; commandLineArguments[1 .. length])
         { 
            if (argument == "Mortimer")
            {
               writefln("Mortimer?  Are you kidding?  I'm done with this.");
               break functionLoop;
            }

            greeter(argument);
         }
      }
   }
}

If we actually wanted the behavior shown by our first shot at this program, and which we labelled as erroneous, we could use the continue statement.  By default the continue statement would simply go to the next iteration of the argument loop, but we can also specify a loop for it to continue for.

import std.stdio;

void main(char[][] commandLineArguments)
{
   alias void function(char[]) GreeterFunction;
   
   static GreeterFunction[] greeters = 
      [&sayHelloTo, &tellOff, &GreetWithSlang];

   if (commandLineArguments.length > 1)
   {
      functionLoop: 
      foreach (GreeterFunction greeter; greeters)
      {
         argumentLoop:
         foreach (char[] argument; commandLineArguments[1 .. length])
         { 
            if (argument == "Mortimer")
            {
               writefln("Mortimer?  Are you kidding?  I'm done with this.");
               continue functionLoop;
            }

            greeter(argument);
         }
      }
   }
}

14.  Other Loops: Flexibility, Along With Some Basic Input and Scoping

As a follow up to the previous sections, for and foreach are not the only types of loops.  There are also simpler loops, which give us further flexibility, though are less syntactically convenient.

The more common of these is the "while" loop.  

With the for loop we outlined three things the loop needed:


An initial state for the counter.
A test to see if the counter is still valid.
An update for the counter.


The for loop gave us a convenient form for listing these three elements.  With the while loop they're still present, but individually located.

Typically the initial state is set up just before entering the loop.  The test is located in parentheses after the "while" keyword.  The update may occur anywhere within the body of the loop.

Let's convert a for loop to a while loop.  This loop is fairly simple.  It counts from one to ten, outputting each number.

for (int count = 0; count  dmd myprogram.d greeting.d -w

-----------------------------------
wtd
Fri Jul 22, 2005 11:10 pm


-----------------------------------
21.  More Complex Data

So far the only data we've looked at has been fairly simple.  Either simple numerical values, arrays or strings.

Let's consider a simple program.  We want to keep track of student information for a class.  Each student has a first name, a last name, and an array of ten grades.   

We could simply write something like:

void main()
{
   static char[][] firstNames = ["Chris",  "Bob"   ];
   static char[][] lastNames  = ["",       "Smith" ];
   static  int[][10] grades   = [[87, 67], [65, 43]];  
}

That's messy.  We can relatively easily match a first name to the wrong last name, for instance.

A better approach would be to put all of the information for any given student in a single variable.  This variable would have to contain multiple pieces of data of various types.  Additionally, we want to be able to give meaningful names to each individual component.

Structs give us this capability.  Let's start out by constructing the basic skeleton of a struct to contain this data.

struct StudentRecord
{

}

The individual members of a struct are declared just as variables are.

struct StudentRecord
{
   char[]  firstName;
   char[]  lastName;
   int[10] grades;
}

We can then declare a variable of this new type.

void main()
{
   StudentRecord aStudent;
}

The individual components of the struct can then be initialized.

void main()
{
   StudentRecord aStudent;

   aStudent.firstName = "Bob";
   aStudent.lastName  = "Smith";
   aStudent.grades[0] = 97;
   aStudent.grades[1] = 78;
}

You'll note that we've used the dot syntax we'd used previously with modules to access the members of a struct.

Structs can also be initialized statically, as arrays can be.

void main()
{
   static StudentRecord aStudent =
      {firstName: "Bob", lastName: "Smith",
       grades: [97, 78]};
}

Here we've used the names of each member to clarify exactly what each value is initializing.  In fact, we could switch them around, and it still works.

void main()
{
   static StudentRecord aStudent =
      {grades: [97, 78], 
       lastName: "Smith", firstName: "Bob"};
}

However, if we so wish, we can initialize a struct based purely on the order of the members.

void main()
{
   static StudentRecord aStudent = 
      {"Bob", "Smith", [97, 78]};
}

Sometimes we want default values, so that even if we don't initialize a struct with a value explicitly, it is still there.  For instance, an empty string might be a valid default value for a last name.  Rather than write the following each time:

void main()
{
   static StudentRecord aStudent =
      {firstName: "Bob", lastName: "Smith",
       grades: [97, 78]};
}

We can simply write our struct definition as:

struct StudentRecord
{
   char[]  firstName;
   char[]  lastName = "";
   int[10] grades;
}

And then initialize the struct as:

void main()
{
   static StudentRecord aStudent =
      {firstName: "Bob", grades: [97, 78]};
}

22.  Structs and Functions, with an Appearance by Casts and Properties

Now, let's say we wish to write a function which will find the average of the grades in a StudentRecord struct.

double averageGradeForStudent(StudentRecord aStudent)
{
   int sumOfGrades = 0;

   foreach (int grade; aStudent.grades)
      sumOfGrades += grade;

   return sumOfGrades / aStudent.grades.length;
}

There's just one problem here.  When we divide two integers, as both the length of the grades array and the sum of the grades are, we get another integer.

Three divided by two, for instance, when both are integers, will yield one.  If at least one of those two numbers is a floating point number, the result will also be a floating point number.  In this case, one and a half.

Clearly, this is important for getting a precise average grade.  

Fortunately we can cast an integer into a double precision floating point number.

double averageGradeForStudent(StudentRecord aStudent)
{
   int sumOfGrades = 0;

   foreach (int grade; aStudent.grades)
      sumOfGrades += grade;

   return sumOfGrades / cast(double)aStudent.grades.length;
}

More than just member variables, though, structs may contain member functions which operate on those variables.  With our current example, we have a faiely large amount of extra naming going on to explain the relationship between the function and the struct.  If the function were a member of the struct, that relationship would be self-evident.

struct StudentRecord
{
   char[]  firstName;
   char[]  lastName = "";
   int[10] grades;

   double averageGrade()
   {
      int sumOfGrades = 0;

      foreach (int grade; grades)
         sumOfGrades += grade;

      return sumOfGrades / cast(double)grades.length;
   }
}

Now we can simply write:

void main()
{
   static StudentRecord aStudent =
      {firstName: "Bob", lastName: "Smith",
       grades: [97, 78, 54, 86, 70, 99, 90, 83, 81, 96]};

   writefln("The average grade for %s is %0.1f.", 
            aStudent.firstName,
            aStudent.averageGrade());
}

Thus far, we've only seen properties on simple data structures, like arrays, and always properties that are built into the language.

We can, however, create our own properties for our own types of data.  Indeed, any function which takes no arguments can be called as a property.  Thus we could rewrite the previous example as:

void main()
{
   static StudentRecord aStudent =
      {firstName: "Bob", lastName: "Smith",
       grades: [97, 78, 54, 86, 70, 99, 90, 83, 81, 96]};

   writefln("The average grade for %s is %0.1f.", 
            aStudent.firstName,
            aStudent.averageGrade);
}

This gives us the appearance that "averageGrade" is just another member variable of the struct, yet it's really a dynamic piece of information.  Change the individual grades, and the average grade will change as well.

You may also note the use of a slightly more complex format specifier.

%0.1f

This indicates a floating point number with one digit after the decimal point.  By indicating zero digits before the decimal point, only as much space will be taken up as it necessary.

23.  Templates

In the previous section, I explained that it was necessary within the averageGrade property to cast at least one operand in the division to a double precision floating point number.

This would not, in fact be necessary if the grades themselves and thereby the sum of those grades, was naturally a double precision floating point number.

We could rewrite the code for this effect.

struct StudentRecord
{
   char[]  firstName;
   char[]  lastName = "";
   double[10] grades;

   double averageGrade()
   {
      double sumOfGrades = 0;

      foreach (double grade; grades)
         sumOfGrades += grade;

      return sumOfGrades / grades.length;
   }
}

We may, however, wish to still use a version of the StudentRecord with ints for grades.

Fortunately we can have it both ways.  Rather than creating the StudentRecord struct directly, we'll create it as part of a template.

template StudentRecordTemplate(GradeType)
{
   struct StudentRecord
   {
      char[]  firstName;
      char[]  lastName = "";
      GradeType[10] grades;

      GradeType averageGrade()
      {
         GradeType sumOfGrades = 0;

         foreach (GradeType grade; grades)
            sumOfGrades += grade;

         return sumOfGrades / grades.length;
      }
   }
}

You'll notice the use of the "GradeTye" type in several places.  When the template is used, this will be replaced by the type provided.  Let's look at a simple use of this.

void main()
{
   static StudentRecordTemplate!(double).StudentRecord aStudent =
      {firstName: "Bob", lastName: "Smith",
       grades: [97, 78, 54, 86, 70, 99, 90, 83, 81, 96]};

   writefln("The average grade for %s is %0.1f.", 
            aStudent.firstName,
            aStudent.averageGrade);
}

With:

StudentRecordTemplate!(double)

We've instantianted, or created an instance of the template, with GradeType as double.

We can then access the StudentRecord struct as a member of that template instance.  From there, everything proceeds as expected.

This gets a bit messy, though, syntactically speaking.  Fortunately, we can use the alias statement to clear things up.

alias StudentRecordTemplate!(int).StudentRecord 
      StudentRecord;

Then our code looks like:

void main()
{
   alias StudentRecordTemplate!(int).StudentRecord 
         StudentRecord;

   static StudentRecord aStudent =
      {firstName: "Bob", lastName: "Smith",
       grades: [97, 78, 54, 86, 70, 99, 90, 83, 81, 96]};

   writefln("The average grade for %s is %0.1f.", 
            aStudent.firstName,
            aStudent.averageGrade);
}

24.  Specialization

Of course, if we create an instance of the template using integers, we'll have the same integer division problem.

What we really need is a way to speialize the template for a particular type, while keeping the generic template for everything else.  Thankfully, this is relatively easy.

template StudentRecordTemplate(GradeType : int)
{
   struct StudentRecord
   {
      char[]  firstName;
      char[]  lastName = "";
      GradeType[10] grades;

      double averageGrade()
      {
         GradeType sumOfGrades = 0;

         foreach (GradeType grade; grades)
            sumOfGrades += grade;

         return sumOfGrades / cast(double)grades.length;
      }
   }
}

This looks nearly identical to the previous template, but change the type of the averageGrade property and adds a cast where necessary.

We can provide any number of specializations, and the compiler will choose the best match.

25.  With

In any remotely complex program, there will be times when we need to access several members of a struct or template instance.

Consider:

void main()
{
   alias StudentRecordTemplate!(int).StudentRecord 
         StudentRecord;

   static StudentRecord aStudent =
      {firstName: "Bob", lastName: "Smith",
       grades: [97, 78, 54, 86, 70, 99, 90, 83, 81, 96]};

   if (aStudent.lastName != "")
      writefln("The average grade for %s %s is %0.1f.", 
               aStudent.firstName,
               aStudent.lastName,
               aStudent.averageGrade);
   else
      writefln("The average grade for %s is %0.1f.", 
               aStudent.firstName,
               aStudent.averageGrade);
}

Instead, we could simply write:

void main()
{
   alias StudentRecordTemplate!(int).StudentRecord 
         StudentRecord;

   static StudentRecord aStudent =
      {firstName: "Bob", lastName: "Smith",
       grades: [97, 78, 54, 86, 70, 
                99, 90, 83, 81, 96]};

   with (aStudent)
   {
      if (lastName != "")
         writefln("The average grade for %s %s is %0.1f.", 
                  firstName,
                  lastName,
                  averageGrade);
      else
         writefln("The average grade for %s is %0.1f.", 
                  firstName,
                  averageGrade);
   }
}

We can use "with" in a nested pattern as well.  One of the interesting uses is in conjunction with templates.  Rather than an alias to access the StudentRecord struct, we could use with to gain easy access to the internals of the template instance.

void main()
{
   with (StudentRecordTemplate!(int))
   {
      static StudentRecord aStudent =
         {firstName: "Bob", lastName: "Smith",
          grades: [97, 78, 54, 86, 70, 
                   99, 90, 83, 81, 96]};

      with (aStudent)
      {
         if (lastName != "")
            writefln("The average grade for %s %s is %0.1f.", 
                     firstName,
                     lastName,
                     averageGrade);
         else
            writefln("The average grade for %s is %0.1f.", 
                     firstName,
                     averageGrade);
      }
   }
}

One caveat to keep in mind is that any names in, for instance "aStudent" will override anything else you may have created up to that point.

-----------------------------------
Cyril22
Mon Jul 01, 2013 6:37 am

Re: D: A Newbie-Oriented Tutorial
-----------------------------------
Thanx for sharing this nice and informative post.
