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!
// 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.
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
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
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.
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:
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:
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.
Top comments (1)
D excels at meta programming. It also gets co/contra verance right, C# has recently made progress here. Builtin unittests are a great convenience.
D has also did a great job creating composable algorithms.