handson-dlang/hands-on_dlang.md
2018-06-18 15:19:53 +02:00

38 KiB
Raw Permalink Blame History

Hands-On DLang

Setup

Installing DMD and DUB

OS X

brew install dmd
brew install dub
Installing locally using the install script
curl -fsS https://dlang.org/install.sh | bash -s dmd
echo "~/.dlang/dmd-2.080.1/activate" >> ~/.profile # Add dmd and dub to PATH on starting a bash shell
Installing using the installer

Windows

Visual Studio Code is the recommended editor, because it has the best D integration at the moment. If you want to use another editor or IDE, that is perfectly fine. However, instructions will only be provided for Visual Studio Code.

Installation of Visual Studio Code

Download and install Visual Studio Code from here: https://code.visualstudio.com/. OS X users can also install it using Homebrew:

brew tap caskroom/cask
brew cask install visual-studio-code

Extension setup

  • Open the Extension view in the sidebar:

    Operating system Shortcut
    OS X ⌘ + ⇧ + X
    Windows ⌃ + ⇧ + X
  • Install the extension “D Programming Language (code-d)” (requires that git is installed).

  • Restart Visual Studio Code.

Project setup and compiling

Without DUB

No special project setup is needed, you can order your source files any way you like. You can compile them by invoking DMD:

dmd path/to/sourcefile1.d path/to/sourcefile2.d …

This generates a binary with the same name as the first sourcefile provided.

With DUB

To init a DUB project, just run

dub init

inside your projects root folder. This runs an interactive dialog which allows you to specify some settings for the project (the project name etc.). Depending on your choice, these settings are saved in a file called either dub.json or dub.sdl.

By default, the source files will be contained in the source subdirectory.

To compile the project, simply run

dub build

in the projects root directory. This generates a binary with the same name as the configured project name. It is also possible to compile and run in one step, just run

dub run

in the projects root directory.

Basics

Hello World

import std.stdio;

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

Imports and modules

D has the concept of modules and packages. By importing a certain module with the import statement, all public symbols from module become available. The standard library, called Phobos, is located in the std package. E.g. in order to import the file module from Phobos, you would write:

import std.file;

Selective imports

It is possible (and often good style) to import symbols selectively from a module:

import std.stdio: writeln, writefln;

Scoped imports

It is not necessary to place imports at the beginning of a file. They can be located anywhere in the code. If they appear inside a certain scope (delimited by braces), the imported symbols are only available inside that scope. Here is an alternative version of the hello world program:

void main() {
    import std.stdio: writeln;
    writeln("Hello World");
}
/* writeln is not available outside of the main function */

Imports match files and directories

The module system is entirely based on files. E.g. my.thing refers to a file thing.d in the folder my/.

Basic Types

D has the following basic types:

Data types Size
bool, byte, ubyte, char 8-bit
short, ushort, wchar 16-bit
int, uint, dchar, float 32-bit
long, ulong, double 64-bit
real >= 64-bit (generally 64-bit, but 80-bit on Intel x86 32-bit)

char represents UTF-8 characters, wcharrepresents UTF-16 characters, and dchar represents UTF-32 characters.

Type conversion

For integer types, automatic type conversion is only allowed if no precision is lost (e.g. int to long). All conversion between floating point types are allowed (e.g. double to float).

Manual type conversion is achieved with the cast expression:

long a = 1;
int b = cast(int) a;

Type properties

All types have a property .init to which variables of that type are initialized, if they are not initialized explicitly. For integer types, this is 0 and for floating point types it is nan.

Every type also has a .stringof property which yields its name as a string.

Integer types have some more properties:

Property Description
.max The maximum value the type can hold
.min The minimum value the type can hold

And so do floating point types:

Property Description
.max The maximum value the type can hold
.min_normal The smallest representable normalized value that is not 0
.nan NaN value
.infinity Infinity value
.dig number of decimal digits of precision
.mant_dig number of bits in mantissa

Indexing

For indexing, usually the alias type size_t is used, which is large enough to represent an offset into all addressable memory.

Variable declarations

Variables are declared by writing the type followed by the variable name:

int myVar;

They can also be explicitly initialized:

int myVar = 42;

It is also possible to declare several variables at once:

int myVar, someOtherVar;

D has automatic type deduction, so when explicitly initializing a variable, it is not necessary to mention the type. Instead we can use the auto keyword:

auto myVar = 42;

Here is a combination of the above notations:

auto myInt = 42, myFloat = 4.2f;

Mutability

Objects in D are mutable by default, but is possible to change this using type qualifiers:

immutable

An object declared as immutable is enforced by the compiler to never change its value.

immutable int a;
a = 5; // error

immutable objects are implicitly shared across threads, because the can never change their value and thus race conditions are impossible.

const

const objects also can not be modified, but this is enforced only in the current scope. This means, that the object could be modified from a different scope. Both mutable and immutable objects implicitly convert to const objects:

void foo(const char[] s)
{
    // Do something with s
}
// Both calls are valid, thanks to const
foo("abcd"); // a string is an immutable array of char
foo("abcd".dup); // dup creates a mutable copy

Both immutable and const are transitive, i.e. the apply recursively to all subcomponents of a type they are applied to.

Functions

The basic syntax for functions is very similar to C:

int add(int lhs, int rhs) {
    return lhs + rhs;
}

Return type deduction

A functions return type can be defined to be auto. In this case, the return type will be inferred. Multiple return statements are possible, but must return compatible types.

auto add(int lhs, int rhs) { // returns `int`
    return lhs + rhs;
}

auto lessOrEqual(int lhs, int rhs) { // returns `double`
    if (lhs <= rhs)
        return 0;
    else
        return 1.0;
}

Default arguments

Those also work the same as in C and other languages:

void plot(string msg, string color = "red") {
    /* ... */
}
plot("D rocks");
plot("D rocks", "blue");

Local functions

It is possible to define functions locally (even inside other functions). Those functions are not visible outside their parents scope.

void fun() {
    int local = 10;
    int fun_secret() {
        local++; // that's legal
    }
    /* … */
}
static assert(!__traits(compiles, fun_secret())); // fun_secret is not visible here

Memory and pointers

D uses a garbage collector by default, but is also possible to do manual memory management if needed.

D provides pointer types T* like in C:

int a;
int* b = &a; // b contains address of a
auto c = &a; // c is int* and contains address of a

To allocate a new memory block on the garbage collected heap, use the new operator:

int* a = new int;

Memory safety

In general, pointer arithmetic like in C is allowed. This results in the usual safety issues. To counter this, D defines 3 safety levels for functions: @safe, @trusted, and @system. The default is @system, which gives no safety guarantees. Functions annotated with @safe are only allowed to call other @safe and @trusted functions and it is not possible to do pointer arithmetic in them:

void main() @safe {
    int a = 5;
    int* p = &a;
    int* c = p + 5; // error
}

@trusted functions are functions that are manually verified to provide an @safe interface. They create a bridge between @safe code and dirty low-level code. Only use them very carefully!

Structs

One way to create custom data types in D is with structs:

struct Person {
    int age;
    int height;
}

Unless created with the new operator, sturcts are always constructed on the stack and copied by value in assignments and as parameters to function calls.

auto p = Person(30, 180);
auto t = p; // copy

You can also define a custom constructor:

struct Person {
    int age;
    int height;
    this(int age, int height) {
        this.age = age;
        this.height = height;
    }
}

Member functions

structs can have member functions. By default, they are public and accessible from outside. By marking them private, you can limit access to functions in the same module (different from C++ / Java etc.!):

struct Person {
    void doStuff() {
        /* … */
    }
    private void privateStuff() {
        /* … */
    }
}
auto p = Person();
p.doStuff(); // call method doStuff
p.privateStuff(); // forbidden

const member functions

Member functions declared const can not modify any members. They can be called on immutable and const objects.

static member functions

They work basically the same as in C etc.

Arrays

D has two types of arrays, static arrays and dynamic arrays. Both of them are bounds checked unless this feature is explicitly switched of with the compiler flag --boundscheck=off.

Static arrays

Static arrays are stored on he stack or in static memory, depending on where they are defined. They have a fixed, compile time known length. The length is part of the type:

int[8] arr;

Dynamic arrays

Dynamic arrays are stored on the heap and have a variable length, which can change during runtime. A dynamic array is created with the new expression:

auto size = 8;
int[] arr = new int[size];

The type int[] is called a slice of int. Slices will be explained in more detail in the next section.

Creating multidimensional arrays is also easy:

auto matrix = new int[3][3];

Array operations and properties

Array concatenation is done with the ~ operator, which creates a new dynamic array.

Mathematical operations can be applied to whole arrays using a syntax like c[] = a[] + b[], for example. This adds all elements of a and b so that c[0] = a[0] + b[0], c[1] = a[1] + b[1], etc. It is also possible to perform operations on a whole array with a single value:

a[] *= 2; // multiple all elements by 2
a[] %= 26; // calculate the modulo by 26 for all a's

These operations can be optimized by the compiler using SIMD instructions.

Both static and dynamic arrays provide the property .length, which is read-only for static arrays, but can be used in the case of dynamic arrays to change its size dynamically. The property .dup creates a copy of the array.

When indexing an array through the arr[idx] syntax, a special $ symbol denotes an array's length. For example, arr[$ - 1] references the last element and is a short form for arr[arr.length - 1].

Slices

Slices are object of the type T[]for any given type `T. Slices provide a view to a subset of an array.

A slice consists basically of two members:

T* ptr;
size_t length;

As we have already seen in the previous section, we can get slices by allocating a new dynamic array:

auto arr = new int[5];
assert(arr.length == 5)

The slice does not own the memory, it is managed by the garbage collector. The slice is just a view on the memory.

You can also get slices to already existing memory:

auto arr = new int[5];
auto newArr = arr;
auto smallerViewArr = arr[1 .. 4]; // index 4 is not included
assert(smallerViewArr.length == 3);
assert(newArr.length == 5);
smallerViewArr[0] = 10;
assert(newArr[1] == 10 && arr[1] == 10);

Again, it is important to keep in mind, that this is only a view to memory. No memory is copied.

Alias and strings

By using the alias statement , we can create new “names” for existing types:

alias string = immutable(char)[];

This works very similar to typedef from C / C++.

The above definition of string is actually the definition that is used by D. This means that strings are just mutable slices of immutable chars.

Control flow

if…else

Very similar to how it is defined in other languages:

if (a == 5) {
    writeln("Condition is met");
} else if (a > 10) {
    writeln("Another condition is met");
} else {
    writeln("Nothing is met!");
}

switch…case

Also very similar to how it is defined in other languages, but for it works for integer types, bools and strings.

string myString;
/* … */
switch(myString) {
    case "foo":
        writeln(`Cool, myString was "foo"`);
        break;
    default:
        writeln("Meh, myString was something boring");
        break;
}

For integer types, it is also possible to define ranges:

int c = 5;
switch(c) {
    case 0: .. case 9:
        writeln(c, " is within 0-9");
        break; // necessary!
    case 10:
        writeln("A Ten!");
        break;
    default: // if nothing else matches
        writeln("Nothing");
        break;
}

Old fashioned loops

while-, dowhile- and classical for-loops all work the same as in C++ / Java etc.

Breaking out of outer loops

As usual, you can break out of a loop immediately by using the break keyword. Additionally, you can also break out of outer loops by using labels:

outer:
for (int i = 0; i < 10; ++i) {
    for (int j = 0; j < 5; ++j) {
        /* … */
        break outer; // breaks out of the outer loop
    }
}

foreach loops

D has a foreach loops which allows for much better readable iterations.

Element iteration

We can easily iterate over slices using foreach:

auto arr = new int[5];
foreach (e; arr) {
    writeln(e);
}
Access by reference

By default the elements are copied during the iteration. If we want in-place modification, we can use the ref qualifier:

auto arr = new int[5];
foreach (ref e; arr) {
    e = 5;
}
Iterate n times

It is easy to write iterations, which should be executed n times by using the .. syntax:

foreach (i; 0 .. 3) {
    writeln(i);
}
// prints 0 1 2
Iteration with index counter

For slices, it's also possible to access a separate index variable:

foreach (i, e; [4, 5, 6]) {
    writeln(i, ":", e);
}
// prints 0:4 1:5 2:6

Ranges

Ranges are a very important concept for iteration in D. We can use foreach loops, to iterate over ranges:

foreach (element; range) {
    // Loop body
}

If we use foreach with a range, this gets lowered to the compiler to something similar to this:

for (auto __rangeCopy = range; !__rangeCopy.empty; __rangeCopy.popFront()) {
    auto element = __rangeCopy.front;
    // Loop body...
}

This leads us to what ranges (or more specific InputRanges) actually are: Anything, that implements the member functions needed by the above lowering:

interface InputRange(E) {
    bool empty() @property;
    E front() @property;
    void popFront();
}

However, ranges do not need to implement such an interface in terms of inheritance, they just have to provide the above member functions. Typically, ranges are implemented as structs (because most of the time, ranges should be value types), but is also possible to implement them as classes, which will be introduced later.

Laziness

Ranges are lazy. They won't be evaluated until requested. Hence, a range from an infinite range can be taken:

42.repeat.take(3).writeln; // [42, 42, 42]

Copying ranges

Copying a range by just using the assignment operator might not have the desired effect, because iterating over a range can be destructive (i.e. when the range holds internal pointers and a deep copy would be necessary). “copyable” ranges are called ForwardRanges. They need to implement a .save method which returns a copy of the range:

interface ForwardRange(E) : InputRange!E
{
    typeof(this) save();
}

RandomAccessRanges

A RandomAccessRange is a ForwardRange which has a know length for which each element can be access directly:

interface RandomAccessRange(E) : ForwardRange!E
{
     E opIndex(size_t i); // can access elements using range[i] syntax
     size_t length() @property;
}

Slices are the most prominent example of RandomAccessRanges in

Lazy range algorithms

The D standard library provides a huge arsenal of lazy range algorithm functions. Most of them can be found in in the std.range and std.algorithm packages.

Associative arrays

D has builtin hashmaps, which are called associative arrays:

int[string] map; // keys of type string, values of type int
map["key1"] = 10; // insertion or modification, if the key already exists
if ("key1" in map) { // checking if a key is in an associative array
    writeln("key1 is in map");
}
assert(map.length == 1); // associative arrays provide a .length property
map.remove("key1"); // remove a key from an associative array

Classes

D's classes are very similar to Java's classes.

Any class type implicitly inherits from Object.

class Foo { } // implicitly inherits from Object
class Bar : Foo { } // Bar inherits from Foo

classes are usually instantiated on the GC heap using new:

auto bar = new Bar;

In contrast to structs, classes are reference types:

Bar bar = foo; // bar points to foo

Inheritance

If a member function of a base class if overwritten, the override keyword must be used:

class Bar : Foo {
    override void functionFromFoo() {}
}

Classes can only inherit from a single class

Final and abstract member functions

  • A function can be marked final in a base class to disallow overriding it.
  • A function can be declared as abstract to force derived classes to override it.
  • A whole class can be declared as abstract to make sure that it isn't instantiated.
  • super(…) can be used to explicitly call the base constructor.

Checking for identity

The == operator compares the content of two class objects. Checking for identity is done using the is operator. Comparing to null is only possible with this operator:

MyClass c;
if (c == null)  // error
    /* … */
if (c is null)  // ok
    /* … */

Interfaces

interfaces work basically the same as in Java:

interface Animal {
    void makeNoise();
}

class Dog : Animal {
    override void makeNoise() {
        writeln('woof!');
    }
}

auto dog = new Dog;
Animal animal = dog; // implicit cast to interface
animal.makeNoise();

Templates

Template functions

Template functions in D are very similar to C++ template functions:

auto add(T)(T lhs, T rhs) {
    return lhs + rhs;
}

add!int(5,10);
add!float(5.0f, 10.0f);
add!Animal(dog, cat); // error, Animal does not implement +

In the explicit template notation, the add template function would look like this:

template add(T) {
    auto add(T lhs, T rhs) {
        return lhs + rhs;
    }
}

D also allows implicit function template instantiation:

add(5, 10); // same as add!int(5,10)

Templates can have more than one template parameter:

void print(S, T)(S x, T y) {
    writeln(x);
    writeln(y);
}

print!(int, string)(42, "is the best number");

Other templates

Of course, structs, classes and interfaces can also be templates:

struct S(T) {
    /* … */
}

auto s = S!int();

It is also possible to have variable templates:

ubyte[T.sizeof * 8] buffer8(T) = 42;

auto myBuffer = buffer!(int); // create a buffer with space for 8 ints and initialize its elements to 42
static assert(is(typeof(myBuffer) == ubyte[32]));
assert(myBuffer[0] == 42);

This gets lowered to the following explicit template syntax:

template buffer8(T) {
    ubyte[T.sizeof * 8] buffer8 = 42;
}

Template value parameters

So far we have only seen template type parameters. It is also possible to have template value parameters:

struct Animal(string noise) {
    void makeNoise() {
        writeln(noise);
    }
}

auto dog = Animal!"woof!"();
dog.makeNoise(); // woof!

Other template parameters

There are some other types of template parameters: template alias parameters and template sequence parameters. Template alias parameters can be any D symbol (which means basically anything except for basic types):

void print(alias var)() {
    writeln(var);
}

struct S {}
auto x = 42;
print!x();
print!4.2f();
print!"foo"();
print!S(); // error, would be actually legal, but cant pass types to writeln

Template sequence parameters take a variadic number of template arguments:

void print(Args...)(Args args) {
    foreach (arg; args) { // yes, we can iterate over them (loop unrolling)
        writeln(arg);
    }
}

print(42, 3.5f, "foo");
print(); // also valid, prints nothing

Delegates

Functions as arguments

Global functions can be referenced using the function type:

void doSomething(int function(int, int) doer) {
    // call passed function
    doer(5,5);
}

auto add(int a, int b) {
    return a + b;
}

doSomething(&add);

Local functions with context

To reference member functions or local functions, delegates have to be used. You can create closures with this:

auto foo() {
    int x = 42;
    auto local() {
        return x;
    }
    return &local;
}

auto f = foo();
static assert(is(typeof(f) == int delegate() pure nothrow @nogc @safe));
writeln(f()); // 42

Anonymous functions and lambdas

You can write anonymous functions and lambdas like this:

auto add = (int lhs, int rhs) {
    return lhs + rhs;
};

auto lambdaAdd = (int lhs, int rhs) => lhs + rhs;

These are often passed as template arguments in the functional parts of Phobos:

[1, 2, 3].reduce!((a, b) => a + b)();

Exceptions

Exceptions in D are very similar to Exceptions in Java:

class MyException : Exception {
    this(string msg) {
        super("MyException was thrown because of " ~ msg);
    }
}

try {
    throw new MyException("no reason");
}
catch (FileException e) { // not caught here
    /* … */
}
catch (MyException e) { // but here
    /* … */
}
finally { // executed regardless of whether an exception was thrown or not
    /* … */
}

nothrow

The compiler can enforce that a function can not throw an exception. You can achieve this by annotating a function with nothrow:

import std.exception: enforce; // convenience function which throws if `false` is passed

int divide(int a, int b) {
    enforce(b != 0, "Can't divide by 0!");
    return a / b;
}

int divide4By2() nothrow {
    return divide(4, 2); // error, divide my throw
}

Gems

Uniform function call syntax (UFCS)

UFCS is a simple and yet very powerful feature of D. It basically allows that any call to a free function fun(a) can be written as member function call a.fun(). If the compiler sees a.fun() and the type of a does not have a member function fun(), it tries to find a global function whose first parameter matches the type of a.

This makes complex chains of function calls much more readably. Instead of writing

foo(bar(a));

one can write

a.bar().foo();

For functions that take no arguments, it is not necessary to use parenthesis, so any function like that can be used like a property:

import std.uni: toLower;
auto a = "Cool Stuff".toLower;
assert(a == "cool stuff");

UFCS is especially important when dealing with ranges where several algorithms can be put together to perform complex operations, still allowing to write clear and manageable code.

import std.algorithm : group;
import std.range : chain, retro, front, retro;
[1, 2].chain([3, 4]).retro; // 4, 3, 2, 1
[1, 1, 2, 2, 2].group.dropOne.front; // tuple(2, 3u)

Scope guards

Scope guards allow executing statements at certain conditions if the current block is left:

  • scope(exit) will always call the statements.
  • scope(success) statements are called when no exceptions have been thrown.
  • scope(failure) denotes statements that will be called when an exception has been thrown before the block's end.

Using scope guards makes code much cleaner and allows to place resource allocation and clean up code next to each other. They also improve safety because they make sure certain cleanup code is always called independent of which paths are actually taken at runtime.

Scope guards are called in the reverse order they are defined.

void foo() {
    import core.stdc.stdlib : free, malloc;
    int* p = cast(int*) malloc(int.sizeof);
    scope(exit) free(p);
    /* Do some stuff, which potentially might throw, which does not matter,
       p is freed anyways when leaving the scope */
}

Range algorithms

The modules std.range and std.algorithm contain many functions which can be composed to express complex operations in a readable way. They are based on ranges and will work on your data types, if they implement the range interface.

import std.algorithm : canFind, map,
  filter, sort, uniq, joiner, chunkBy, splitter;
import std.array : array, empty;
import std.range : zip;
import std.stdio : writeln;
import std.string : format;

void main() {
    string text = `This tour will give you an
overview of this powerful and expressive systems
programming language which compiles directly
to efficient, *native* machine code.`;

    // splitting predicate
    alias pred = c => canFind(" ,.\n", c);
    // as a good algorithm it just works lazily without allocating memory!
    auto words = text.splitter!pred
            .filter!(a => !a.empty);

    auto wordCharCounts = words
            .map!"a.count";

    // Output the character count per word in a nice way beginning with least chars!
    zip(wordCharCounts, words)
        // convert to array for sorting
        .array()
        .sort()
        // we don't need duplication, right?
        .uniq()
        // put all in one row that have the same char count.
        // chunkBy helps us here by generating a range of ranges
        // that are chunked by the length
        .chunkBy!(a => a[0])
        // those elments will be joined
        // on one line
        .map!(chunk => format("%d -> %s",
            chunk[0],
            // just the words
            chunk[1]
                .map!(a => a[1])
                .joiner(", ")))
        // joiner joins, but lazily!
        // and now the lines with the line feed
        .joiner("\n")
        // pipe to stdout
        .writeln();
}

Unit testing

D has built-in unit tests via a very simple syntax, which can appear anywhere in a D module:

unittest {
    assert(myAbs(-1) == 1);
    assert(myAbs(1)  == 1);
}

Example

import std.stdio : writeln;

struct Vector3 {
    double x;
    double y;
    double z;

    double dot(Vector3 rhs) const {
        return x * rhs.x + y * rhs.y + z * rhs.z;
    }

    // That's okay!
    unittest {
        assert(Vector3(1,0,0).dot(Vector3(0,1,0)) == 0);
    }

    string toString() const {
        import std.string : format;
        return format("x:%.1f y:%.1f z:%.1f", x, y, z);
    }

    // … and that too!
    unittest {
        assert(Vector3(1,0,0).toString() == "x:1.0 y:0.0 z:0.0");
    }
}

void main() {
    Vector3 vec = Vector3(0,1,0);
    writeln(`This vector has been tested: `, vec);
}

// Or just somewhere else
unittest {
    Vector3 vec;
    assert(vec.x == double.init);
}

Executing unit tests

To execute the unit tests, just compile with the -unittest flag. When running the program, the unit tests get run before the main function is executed. When using DUB, just run

dub test

Code coverage

D also supports generating coverage reports out of the box, just compile with the -cov flag. When using DUB, just add the flag --coverage.

String mixins

The mixin expression takes an arbitrary string and compiles it and generates instructions accordingly. This is a compile time feature, so ths string needs to be known at compile time.

mixin("int b = 5"); // compiles just fine
assert(b == 5);

static immutable compileTimeKnownString = q{int c = 5;};
mixin(compileTimeKnownString); // so does this
assert(c == 5);

immutable runTimeString = q{int c =5;};
mixin(runTimeString); // compile error, runTimeString is not known at compile time

Compile Time Function Evaluation (CTFE)

In D, it is possible to execute functions at compile time. This is triggered on the call site, i.e. whenever a function is called in a context where the result needs to be known at compile time, the function will be evaluated at compile time. Some of those contexts are:

  • Template parameters
  • Initialization of static variables
  • Definition of enums (i.e. compile time constants)
  • static assert, static foreach and static if
  • Template constraints

There are some limitations to what a function executed at compile time may do. Essentially, the functions need to be pure in some sense, i.e. they may not have side effects such as IO.

import std.stdio : writeln;

auto sqrt(T)(T x) {
    enum GoodEnough = 0.01;
    import std.math : abs;
    T z = x * x, old = 0;
    int iter;
    while (abs(z - old) > GoodEnough) {
        old = z;
        z -= ((z * z) - x) / (2 * z);
    }

    return z;
}

void main() {
    double n = 4.0;
    writeln("The sqrt of runtime 4 = ",
        sqrt(n));
    static cn = sqrt(4.0); // calculated at compile time
    writeln("The sqrt of compile time 4 = ",
        cn);
}

Conditional compilation

D has several methods of conditional compilation which can be used for code generation. Ds traits and the module std.traits contain many useful tools which can be used with the following constructs.

static if & is

static if conditionally compiles a code block based on a condition that can be evaluated at compile time:

foo(int x)() { // x is a template parameter and thus known at compile time
    static if(x == 0)
        writeln("x is 0");
    else
        writeln()
}

The is expression is used to check for valid types and to compare types:

foo(T)() {
    static if(is(T)) {
        writeln("T is a valid type");
    }
    static if(is(T == int)) {
        writeln("T is int");
    }
    static if(is(T : int)) {
        writeln("T implicitly converts to int");
    }
}

static foreach

static foreach allows you to iterate at compile time in order to generate code. It is best explained by an example: The code

import std.stdio;

void main() {
    static foreach(element; ["foo", "bar", "baz"]) {
        writeln(element);
    } 
}

effectively lowers to

import std.stdio;

void main() {
    writeln("foo");
    writeln("bar");
    writeln("baz");
}

which means there is no loop at runtime.

Template constraints

A template may be defined with any number of constraints that enforce some properties of the template parameters:

void foo(T)(T value)
    if (is(T : long)) { // foo!T only valid if T implicitly converts to long
}

foo(42); // compiles, 42 is of type int which implicitly converts to long
foo("someString"); // compile error, strings do not implicitly convert to long
void bar(T)(T value)
    if (__traits(compiles, {T t = T.init; t.fly();})) { // bar!T only valid if we can initialize T and T has a method called fly which takes no parameters
}

struct MyStruct {
    void fly() {}
}

MyStruct myStruct;

bar(myStruct); // compiles
bar(42); // compile error