About this ‘A Language a Day’ Advent Calendar
This article became a part of my book ‘A Language a Day’, which you can get in both electronic and paper format. Languages covered in the book: C++, Clojure, Crystal, D, Dart, Elixir, Factor, Go, Hack, Hy, Io, Julia, Kotlin, Lua, Mercury, Nim, OCaml, Raku, Rust, Scala, and TypeScript.
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 spawn
s the threads for each data item. The threads execute a lambda function that sleep
s 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
One thought on “D at a Glance — A Language a Day, Advent Calendar Day 21/24”