I came across the D programming language some time ago while researching C++ templates and found D much clearer in syntax and approach. D has very powerful template programming features for generic and metaprogramming, this is what excited me and has drawn me into the language.
D is a static programming language that some regard as an upgrade to C++, D is also influenced by Java and various other programming languages. It aims to have the performance and safety of compiled languages while allowing the user the expressiveness of a dynamic programming language like Python. D embraces different programming paradigms including imperative, object oriented, and functional programming. There is also support for concurrency and parallel programming. More information about the D programming language can be found at the D official website and wikipedia
In this article, we present basic approaches one can take to writing code for numeric computing in D. Our examples focus on two simplified BLAS (Basic Linear Algebra Subroutine) functions. The scal
function is for scaling the elements of an array or vector by some numerical factor and the dot
function calculates the dot product of two vectors (or arrays), see IBM’s ESSL documentation for more details. Our versions do not worry about incrementation different from 1. The code for this article is located in our <a/ href=“https://github.com/ActiveAnalytics/quick-look-at-d" target="_blank”>GitHub repo
Below is one approach you can take for a simplified scal
function using a foreach loop:
/* File name: scal.d */
import std.stdio : writeln;
double[] scal_double(double[] x, in double a)
{
foreach(i, el; x)
{
x[i] *= a;
}
return x;
}
void main(){
double[] x = [1, 2, 3, 4, 5, 6];
writeln(scal_double(x, 3));
}
There are three D compilers, the reference compiler DMD, the GCC-based compiler GDC, and the LLVM compiler LDC. To compile with LDC on Linux:
ldc2 scal.d -boundscheck=off && ./scal
As a general tip, if we are confident that our index remains in the bounds of our array, we can turn off bounds check which gives us a performance boost. One thing you will notice if you go on to write more D code is that the compiler is FAST - much faster than C/C++. You will also notice that if you make a mistake, the error output you get is clear and informative. Features like this make working in the D language feel seamless from code to output
If you have never seen D code before, the import
command is used to obtain the writeln
function from std.stdio
D’s i/o module and double[]
means an array of doubles. The scal_double
function takes in a double array x
, and a double a
and multiplies each element of x
by a
and returns the result as a double array. The curious in
keyword next to the double a
declaration makes it clear that a
is an input to the function, and an error will occur if you attempt to modify it. Effectively in
declares the variable a
as const
exactly how it would be specified in C/C++. So I could have used const
or more strictly immutable
for the double a
parameter with a similar effect, however marking an input parameter as immutable
requires that the variable input also be immutable. D also supports C/C++ style for loops:
for(int i = 0; i < x.length; ++i)
{
x[i] *= a;
}
But what if I want this function to work for types other than doubles? I could violate the don’t repeat yourself (DRY) principle and write more functions substituting double
for my required type or we could use function templates to make the function generic - the same principle as C++ templates but so much easier to write in D.
Below is the scal
function written using D’s function template shorthand:
X[] scal(X)(X[] x, in X a)
{
foreach(i, el; x)
{
x[i] *= a;
}
return x;
}
This same type of shorthand is also valid for structs, classes and interfaces - come on, is this not much nicer and clearer than C++ angle bracket template notation? If you have not come across templates previously, think of X
as a placeholder for any specified type. The long-form template notation is given below:
template scal(X)
{
X[] scal(X[] x, in X a)
{
foreach(i, el; x)
{
x[i] *= a;
}
return x;
}
}
This long-form reveals that the template is eponymous.
Now that we have made the function generic, we can call the function using:
import std.stdio : writeln;
X[] scal(X)(X[] x, in X a)
{
foreach(i, el; x)
{
x[i] *= a;
}
return x;
}
void main(){
float[] x = [1, 2, 3, 4, 5, 6];
/* explicit notation */
writeln(scal!(float)(x, 3));
/* Short cut notation */
writeln(scal(x, 3));
}
D’s full template instantiation notation uses an exclamation mark. At the point you write scal!(float)
a float version of the scal
function is created, the full declaration is then scal!(float)(x, 3)
, note that for single parameter templates, the form scal!float(x, 3)
is also acceptable. The D compiler is clever enough to infer type from scal(x, 3)
.
The simplified dot
BLAS function calculates the dot product of two arrays. Here is the template version using a loop:
X dot(X)(in X[] x, in X[] y)
{
X output = X(0);
foreach(i, el; x)
{
output += el*y[i];
}
return output;
}
If all I am doing is multiplying an array by a number, do I really have to use a loop? It turns out that D has some builtin array sugar for such situations:
X[] scal(X)(X[] x, in X a)
{
x[] *= a;
return x;
}
Array notation for the dot
case is given below:
import std.algorithm.iteration: sum;
X dot(X)(X[] x, X[] y)
{
x[] *= y[];
return sum(x);
}
D also has uniform function call syntax (UFCS) that allows a function to be called using the receiver as the first parameter:
auto x = [1, 2, 3, 4, 5, 6];
writeln(x.scal(3));
It allows us to have chaining notation which dovetails nicely with function programming styles. The auto
keyword is for automatic type deduction, it means we do not have to explicitly specify the type of x
, note however that the output of x.scal(3)
will now be an int
array.
Functional programming (FP) is now popular in emerging programming languages and is being added to those that are already established. Not only does D have FP support, it is the only major programming language where purity in functions can be optionally enforced (in the case of D by using the pure
keyword). The FP version of the scal
function is:
import std.algorithm.iteration: sum, map;
import std.array: array;
X[] scal(X)(X[] x, X a)
{
return x.map!((xi) => xi*a).array();
}
The functional version of dot
:
import std.stdio : writeln;
import std.range : zip;
import std.algorithm.iteration: sum, map;
X dot(X)(X[] x, X[] y)
{
return zip(x, y).map!(a => a[0]*a[1]).sum;
}
void main(){
double[] x = [1, 2, 3, 4, 5, 6];
double[] y = [3, 6, 9, 12, 15, 18];
writeln(dot(x, y));
}
D also has C-style syntax (in fact D is fully compatible with C and can call and can be called from C). The scal
function in pointer style is given below:
import std.stdio : writeln;
void scal(N, X)(N n, in X a, X* x)
{
while(n)
{
*x *= a;
x += 1;
--n;
}
}
void main(){
double[] x = [1, 2, 3, 4, 5, 6];
scal(6, 3, x.ptr);
writeln(x);
}
In D the .ptr
append returns a pointer to the array.
From this article, it should be clear that D is not dogmatic but rather quite accepting of different programming paradigms and it has a flexible syntax. This blog post did not touch on some of the programming approaches available in D for instance objects and inheritance, however the standard library and language reference is available on the D Programming Language official website and Ali Çehreli’s Programming in D book is a fantastic resource for learning how to program in D.