D at a Glance — A Language a Day, Advent Calendar 2019 Day 21/24

About this ‘A Language a Day’ Advent Calendar 2019

Welcome to Day 21 of this year’s A Language a Day Advent Calendar. Today’s topic is introduction to the D programming language.

Facts about the language

Some facts about the D programming language:

  • Re-thinking of C++
  • Static typing and type inference
  • Allows imperative, object-oriented, and functional programming
  • Not directly memory-safe, but offers a memory-safe subset SafeD
  • Appeared in 2001
  • Website: dlang.org

Installing and running D

Obtaining D is easy: download one of the packages for your operating system and install it. There are three compilers offered at the Download page:

  • DMD — official reference compiler
  • GDC — GCC-based D compiler
  • LDC — LLVM-based D compiler

In the rest of the article, the DMD compiler is used.

You can compile the program using dmd and then run it:

$ dmd helloworld.d
$ ./helloworld

Alternatively, use the on-fly compiler and execute the program as a script:

$ rdmd helloworld.d

Hello, World!

The minimal program in the D language looks like this. It resembles the corresponding C or C++ program:

import std.stdio;

void main() {
    writeln("Hello, World!");
}

An interesting thing is that you can move the import directive to the body of the main function:

void main() {
    import std.stdio;
    writeln("Hello, World!");
}

Variables

A variable is introduced by either specifying a type or with the auto keyword.

import std.stdio;

void main() {    
    auto name = "John";
    string greeting = "Hello";
    writeln(greeting, ", ", name, "!"); // Hello, John!
}

The typeof function allows you to make sure you force the type of the existing variable:

auto name = "John"; // inferred as a string
typeof(name) greeting; // also a string
greeting = "Hello";

You can make a constant with the const type modifier, and if you need an immutable variable (that can only be initialised once), use immutable.

Functions

We have already used a function, the main function. To create a function, give it a name, define its return type and list the arguments. If the return type can be inferred by the compiler, use auto. The default value of the argument can be set after the = sign.

import std.stdio;

auto greet(string name, string greeting = "Hello") {
    return greeting ~ ", " ~ name ~ "!";
}

void main() {
    writeln(greet("John"));       // Hello, John!
    writeln(greet("Alla", "Hi")); // Hi, Alla!
}

Functions can be defined locally inside another function. Here is a modification of the same program, where everything happens within main:

void main() {
    import std.stdio;

    auto greet(string name, string greeting = "Hello") {
        return greeting ~ ", " ~ name ~ "!";
    }

    writeln(greet("John"));       // Hello, John!
    writeln(greet("Alla", "Hi")); // Hi, Alla!
}

Notice the ~ doing string concatenation.

A Factorial example

Here is a possible implementation of a factorial function. Notice that in the case of a recursive algorithm, we cannot use auto as a return type. For a factorial, an unsigned integer uint is a good choice of both input and output values.

uint factorial(uint n) {
    return n < 2 ? 1 : n * factorial(n - 1);
}

void main() {
    import std.stdio;

    writeln(factorial(1)); // 1
    writeln(factorial(5)); // 120
    writeln(factorial(7)); // 5040
}

D support a concept of ranges, which you can use to create a non-iterative factorial function:

auto factorial(uint n) {
    auto r = 1;
    foreach (i; 2 .. n + 1) r *= i;
    return r;
}

Notice that this time, the return type is auto as it can be easily inferred by the compiler.

A word on functional programming

The above examples of the factorial functions can be tagged as pure. For the compiler, that means that you want it to make sure the function returns the same result for the same input values and it does not have side effects. Such a function is aligned with the spirit of a functional programming approach (even if you declare and modify variables inside).

uint factorial(uint n) pure {
    return n < 2 ? 1 : n * factorial(n - 1);
}

Unicode support

D strings are UTF-aware. Here is a small program that demonstrates how the string can be interpreted using its representation as a series of bytes or characters:

import std.stdio;
import std.range;
import std.uni;

void main() {
    string s1 = "Köln";
    writeln(s1.length); // 5 bytes
    writeln(s1.walkLength); // 4 codepoints
    writeln(s1.byGrapheme.walkLength); // 4 graphemes

    string s2 = "Ko\u0308ln";
    writeln(s2.length); // 6 buytes
    writeln(s2.walkLength); // 5 codepoints
    writeln(s2.byGrapheme.walkLength); // 4 graphemes
}

Classes

Classes in D resemble classes in C++. It is important to realise that D implements single inheritance only. It also supports interfaces.

A Polymorphic example

Here is a solution of the zoo problem. The Animal class has an abstract method info, which has to be overridden in the children classes. The name data field is defined in Animal and is inherited by both Cat and Dog.

Notice how the constructor is declared with this.

class Animal {
public:
    this(string n) {
        name = n;
    }
    string name;
    abstract string info();
}

class Cat: Animal {
public:
    this(string n) {
        super(n);
    }
    override string info() {
        return name ~ " is a cat.";
    }
}

class Dog: Animal {
public:
    this(string n) {
        super(n);
    }
    override string info() {
        return name ~ " is a dog.";
    }
}

void main() {
    import std.stdio;

    auto zoo = [new Dog("Charlie"), new Cat("Mollie")];
    foreach (x; zoo)
        writeln(x.info());
}

The program prints the following output, correctly dispatching the call of x.info() depending on the type of an object at hand.

Charlie is a dog.
Mollie is a cat.

Concurrency

Concurrency features are implemented in the std.concurrency standard library. To create a new thread, call spawn:

import std.stdio: writeln;
import std.concurrency: spawn, Tid, thisTid;

void f(Tid parent_thread_id) {
    writeln(thisTid);
    writeln("A message from f().");
}

void main() {
    writeln(thisTid);
    auto thread_id = spawn(&f, thisTid);
}

(Note how you can list specific functions to be imported from libraries.)

Here, the thisTid is a property returning the ID of the current thread. In the output, you should be able to see that the values differ for the two calls of writeln(thisTid).

Messages

Thread can send and receive messages. Let us modify the above program so that it sends a message from one thread to another.

import std.stdio: writeln;
import std.concurrency: spawn, Tid, thisTid, send, receive;

void f(Tid parent_thread_id) {
    send(parent_thread_id, 42);
    writeln("A child thread sent a message.");
}

void informer(int i) {
    writeln("A parent received a value: ", i, ".");
}

void main() {
    auto thread_id = spawn(&f, thisTid);
    receive(&informer);
}

You can also use a lambda function to avoid creating standalone functions:

void main() {
    spawn(&f, thisTid);
    receive(
        (int i) {
            writeln("A parent received a value: ", i, ".");
        }
    );
}

Sleep Sort

The Sleep sort implementation shown below spawns the threads for each data item. The threads execute a lambda function that sleeps the given number of seconds and prints the number.

import std.stdio;
import std.concurrency;
import core.thread;

void main() {
    auto data = [10, 4, 2, 6, 2, 7, 1, 3];
    foreach (x; data)
        spawn(
            (int x) {
                sleep(x);
                writeln(x);
            }, x);
}

Get more

It is very entertaining learning D when you are familiar with C++. I recommend exploring the following websites to learn more about D:

The source codes of today’s article are available on GitHub.

Next: Day 22. Zig

Leave a Reply

Your email address will not be published. Required fields are marked *