100 Languages Speedrun: Episode 12: D
Tags: dlang
Published: 2021-12-02 17:47:28

For the whole history of computing two kinds of languages coexisted. There aren't any established names for these, so I'll make some up:

* _high ground_ languages - focusing on programmer productivity
* _low ground_ languages - focusing on code efficiency

This division makes sense, as 99% of the code doesn't have to be super efficient. Writing everything in _high ground_ languages would mean we'd run out of computers to run things. Writing everything in _low ground_ languages would mean we'd run out of programmers. A healthy mix we adopted lets us have both.

But this situation is not symmetrical. Overwhelming majority of languages are _high ground_ languages. Meanwhile the _low ground_ it's been just assembly, C, and C++, and nobody's happy about that. Everything else never got to even 1% of popularity of these.

There's been many attempts at challenging this situation. First, _middle ground_ languages emerged like Java, C#, and Go. These are memory-managed and JIT-compiled and often match _low ground_ languages in artificial microbenchmarks. Contrary to our hopes, they never got to the point of seriously challenging the _low ground_ languages, as their memory use is typically twice that of C/C++, and even best GC makes execution times highly unpredictable.

Over time there's been many attempts at challenging C/C++ dominance on the _low ground_. Most recently it's been Rust, which seems to be getting limited traction (and it's even arguable if it's a _low ground_ or _middle ground_ language). But long before that, one somewhat notable _low ground_ attempt that eventually failed was [D programming language](https://en.wikipedia.org/wiki/D_(programming_language\)). Even its name was meant as a challenge.

### Hello, World!

But first, let's take a look at Hello, World!

```c++
// hello world in D
import std.stdio;

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

OK, nothing remarkable here.

### Language Quality Checks

Let's do the usual 💩 test:
* is there something like `console.log` for just printing complex types
* does `==` work for complex types
* is Unicode is properly supported.

```c++
import std.stdio;
import std.string;
import std.utf;

void main() {
  int[] arr1 = [1,2,3];
  int[] arr2 = [1,2,3];
  int[] arr3 = [4,5,6];
  string s1 = "hello";
  string s2 = "hello";
  string s3 = "Żółw";
  string s4 = "💩";

  writefln("Printing arrays: %s", arr1);
  writefln("Array equality: %s %s", arr1 == arr2, arr1 == arr3);
  writefln("String equality: %s %s", s1 == s2, s1 == s3);
  writefln("Unicode length: %s %s %s", s1.length, s3.length, s4.length);
  writefln("Unicode length: %s %s %s", std.utf.count(s1), std.utf.count(s3), std.utf.count(s4));
  writefln("Unicode upcase %s", toUpper(s3));
}
```

It prints:

```
Printing arrays: [1, 2, 3]
Array equality: true false
String equality: true false
Unicode length: 5 7 4
Unicode length: 5 4 1
Unicode upcase ŻÓŁW
```

So `==` is good, we have some sort of `console.log`, and UTF8 is half-supported. `.length` returns the number of bytes, and we need special functions to count actual characters. Could be worse.

### Fibonacci

```c++
import std.stdio;

int fib(int n) {
  if (n <= 2) {
    return 1;
  }
  return fib(n - 1) + fib(n - 2);
}

void main() {
  for (int i = 1; i <= 40; i++) {
    writeln(fib(i));
  }
}
```

This might as well be C++.

### FizzBuzz

```c++
import std.stdio;
import std.conv;

string fizzbuzz(int n) {
  if (n % 15 == 0) {
    return "FizzBuzz";
  } else if (n % 3 == 0) {
    return "Fizz";
  } else if (n % 5 == 0) {
    return "Buzz";
  } else {
    return to!string(n);
  }
}

void main() {
  for (int i = 1; i <= 100; i++) {
    writeln(fizzbuzz(i));
  }
}
```

So far it does not impress. There's some weird `to!string` syntax for conversions, the rest looks pretty much like C++ code.

Now this is fully intentional. D was trying to replace C/C++, so it was trying to appeal to C/C++ programmers with familiar syntax. It's also what Java, JavaScript, and C# did.

Notably some newer languages like Go and Rust targeting the same low/middle ground have been much more willing to go for unfamiliar syntax.

### Classes

Classes work pretty much like C++ classes, except for fairly weird operator overloading.

```c++
import std.stdio;
import std.format;

class Point {
  float x,y;

  this(float x, float y) {
    this.x = x;
    this.y = y;
  }

  Point add(Point p) {
    return new Point(this.x + p.x, this.y + p.y);
  }

  override string toString() {
    return format("<%f,%f>", x, y);
  }
}

void main() {
  auto a = new Point(2.0, 3.0);
  auto b = new Point(3.5, 7.0);

  writefln("%s + %s = %s", a, b, a.add(b));
}
```

If we run this, we get more or less what we expected to:

```
$ dmd point.d
$ ./point
<2.000000,3.000000> + <3.500000,7.000000> = <5.500000,10.000000>
```

### Memory Management

OK, so far it's been completely uninteresting. Let's look at something a bit spicier - how D manages memory.

So D by default GCs everything like Java. But you can also exclude some memory areas from GC, have non-GC threads, non-GC functions and so on.

I honestly can't tell how well that balances convenience of normal garbage collected code with any special needs code that might want to avoid GC, as there was never any major software written in D which we could use to judge it.

We can explicitly mark a function as no-GC:

```c++
import std.stdio;
import std.format;
import std.math;

class Point {
  float x,y;

  this(float x, float y) {
    this.x = x;
    this.y = y;
  }

  Point add(Point p) {
    return new Point(this.x + p.x, this.y + p.y);
  }

  override string toString() {
    return format("<%f,%f>", x, y);
  }

  @nogc float size() {
    return sqrt(x*x + y*y);
  }
}

void main() {
  auto a = new Point(2.0, 3.0);
  auto b = new Point(3.5, 7.0);

  writefln("%s + %s = %s", a, b, a.add(b));
  writefln("Length of %s if %f", a, a.size());
}
```

If we try to mark anything that might potentially allocate as `@nogc`, compiler will return an error like:

```
gc.d(13): Error: `@nogc` function `gc.Point.add` cannot call non-@nogc constructor `gc.Point.this`
gc.d(17): Error: `@nogc` function `gc.Point.toString` cannot call non-@nogc function `std.format.format!(char, float, float).format`
```

I'm not really sure how practical that is beyond toy examples.

### Generics

Unlike C++ where they're the same thing, D has generics using type variables system for simple cases, and a separate template system for more complex things.

Here's a generic class:

```c++
import std.stdio;
import std.format;
import std.math;

class Point(T) {
  T x,y;

  this(T x, T y) {
    this.x = x;
    this.y = y;
  }

  Point add(Point p) {
    return new Point(this.x + p.x, this.y + p.y);
  }

  override string toString() {
    return format("<%s,%s>", x, y);
  }
}

void main() {
  auto a = new Point!int(400, 300);
  auto b = new Point!int(-200, 100);
  auto c = new Point!double(3.5, 7.0);
  auto d = new Point!double(1.5, 3.0);

  writefln("%s + %s = %s", a, b, a.add(b));
  writefln("%s + %s = %s", c, d, c.add(d));
}
```

It prints the following:

```
<400,300> + <-200,100> = <200,400>
<3.5,7> + <1.5,3> = <5,10>
```

I removed `size()` as that's not defined for `int`s.

### Should you use D?

Overall D looks like very conservative attempt at taking C/C++ and creating a cleaned up version. Its most "radical" departure is GC by default, but unlike Java it provides plenty of escape hatches for explicit memory management when it would be beneficial.

D never really got anywhere. For language to go big, it's really helpful to have serious corporate backer (like Go, Java, or C# - D lacked even modest backers like Mozilla-backed Rust or Github-backed CoffeeScript), captive audience (like JavaScript in browsers, or ObjectiveC, Swift on iPhones), or any "killer app" (like Ruby with Rails, or Python with data science). The other part is the really difficult territory is was aiming for - establishing new _high ground_ is relatively easy, _low ground_ languages face enormous challenges even in best of circumstances.

From what I've seen, D isn't doing anything especially well, or especially poorly. If "cleaned up C/C++" is what you're looking for, and you're not bothered by tiny community, you could give D a try.

Go and Rust made some far bolder choices, so it's much easier to talk about their benefits and downsides. Somehow that formula, with some corporate backing, was more successful than what D did.

I expect D to slowly sink into obscurity, but it's still actively trying, so it wouldn't be totally shocking if fortunes turned around for it.

### Code

[All code examples for the series will be in this repository](https://github.com/taw/100-languages-speedrun).

[Code for the D episode is available here](https://github.com/taw/100-languages-speedrun/tree/master/episode-12-d).