The D Programming Language CS 4450 What is D? A system programming language conceived in 1996 as a C++ replacement Combines the power of C++ with the usability of languages like Java, C# and Python If something works in C++, C# or Java, it probably works pretty much the same in D “Often, programming teams will resort to a hybrid approach, where they will mix Python and C++, trying to get the productivity of Python and the performance of C++. The frequency of this approach indicates that there is a large unmet need in the programming language department.” “D intends to fill that need. It combines the ability to do lowlevel manipulation of the machine with the latest technologies in building reliable, maintainable, portable, high-level code. D has moved well ahead of any other language in its abilities to support and integrate multiple paradigms like imperative, OOP, and generic programming.” — Walter Bright, Co-designer of D D is like C++ It supports static local variables It supports pointers ◦ but you almost never need them (we won’t) It has templates ◦ but they’re easier and more flexible than in C++ ◦ it supports compile-time execution It’s fast ◦ compiles to native code It supports operator overloading D is Like Java and C# It supports garbage collection ◦ the default, but you can opt-out It supports explicit, user-defined interfaces ◦ uses “:” for inheritance, like C# It has inner classes, like Java and C# It has delegates ◦ but more powerful than in C# It distinguishes reference and value types It supports properties D is Like Python and FP Languages It has modules and packages ◦ like Python’s It supports nested functions and closures ◦ including anonymous functions It has a pure FP subset ◦ and the usual higher-order functions reduce, map, filter, compose ◦ supports lazy evaluation of function parameters D is for Real Programmers It has software engineering support: ◦ unit testing ◦ contract programming invariants, preconditions, postconditions ◦ built-in constructs for exception-safe transactions Supports concurrent programming ◦ with thread-local storage ◦ both lock and lockless programming Can interoperate with C and C++ D'iving In // hello.d import std.stdio; void main(string[] args) { if (args.length > 1) foreach (a; args[1..$]) writeln("Hello " ~ a); else writeln("Hello, Modern World"); } Chuck-Allisons-iMac-4:CCSC chuck$ dmd hello.d Chuck-Allisons-iMac-4:CCSC chuck$ ./hello Hello, Modern World Chuck-Allisons-iMac-4:CCSC chuck$ ./hello John Jane Hello John Hello Jane Primitive Data Types Type void bool byte ubyte short ushort int uint long ulong float double real char wchar dchar Meaning no value Boolean value signed 8 bits unsigned 8 bits signed 16 bits unsigned 16 bits signed 32 bits unsigned 32 bits signed 64 bits unsigned 64 bits 32-bit floating-point 64-bit floating-point largest in hardware unsigned 8-bit UTF-8 unsigned 16-bit UTF-16 unsigned 32-bit UTF-32 Default (.init) n/a false 0 0 0 0 0 0 0 0 float. nan double. nan real. nan 0xFF 0xFFFF 0x0000FFFF Literals and Type Inference Uses the same suffixes as C/C++ ◦ f, l, u, etc. Uses the same prefixes ◦ 0123, 0x1CF ◦ Adds binary: 0b01001 Type inference with auto: auto targetSalary = 15_000_000; auto sq = (double x) { return x*x; }; Arrays Static arrays ◦ Similar to arrays in C/C++ ◦ int a[3]; ◦ int[3] a;// Same thing Dynamic Arrays ◦ Similar to vectors, ArrayLists, etc. See staticarray.d Initialization: ◦ int[3] a = [1:2, 3]; // [0,2,3] Selected Array Operations Indexing with [ ] Concatenation with ~ Copying with a.dup Assignment with = Length with .length ◦ $ = .length in index expressions (a[2..$]) ◦ m..n is an array slice (excludes n) ◦ can assign to .length to resize dynamic arrays Array-wise operations (mathematical vector operations) See mergesort.d, vector.d Strings Strings are dynamic arrays of immutable characters ◦ alias immutable(char)[] string; ◦ string char ◦ wstring wchar ◦ dstring dchar (UTF-8) (UTF-16) (UTF-32) foreach visits each character: ◦ foreach (c; s) … Associative Arrays A map (aka “dictionary”, “hash”, etc.) ◦ Except it’s a built-in type (like dict in Python) Uses array syntax: void main() { int[string] mymap = ["one":1, "two":2]; mymap["three"] = 3; foreach (k,v; mymap) writef("%s:%s ",k,v); } /* Output: three:3 one:1 two:2 */ Word Count Program Output /* Abbreviated output from the Gettysburg Address: But,: 1 Four: 1 … who: 3 will: 1 work: 1 world: 1 years: 1 */ A Word Count Program import std.stdio, std.array, std.file; void wc(string filename) { string[] words = split(cast(string) read(filename)); int[string] counts; foreach (word; words) ++counts[word]; foreach (w; counts.keys.sort) writefln("%s: %d", w, counts[w]); } void main(string[] args) { if (args.length == 2) wc(args[1]); } Tuples Defined in std.typecons Tuple!(type1,type2,..) Helper function: ◦ tuple(val1,val2,…) Can use 0-based position numbers Can also use field names See tuple.d Data Qualifiers For function parameters: ◦ in | out | inout ◦ ref ◦ lazy General declaration qualifiers: ◦ const ◦ immutable Lazy Parameters import std.stdio; void f(bool b, lazy void g) { if (b) { g(); } } void main() { f(false, writeln("executing g")); f(true, writeln("executing g")); } /* Output: executing g */ Closures Nested functions are returned as (dynamic) closures ◦ aka “delegates” (a code-environment pair) ◦ The referencing environment could be a function, class, or object ◦ Escaped activation records are moved from the stack to the garbage-collected heap Plain function pointers also supported: ◦ int function(int) f; (vs. “int (*f)(int);” in C) 20 Plain Function Pointers int f1(int n) {return n+1;} int f2(int n) {return n+2;} int f3(int n) {return n+3;} void main() { auto funs = [&f1, &f2, &f3]; foreach (f; funs) writeln(f(1)); } /* Output: 2 3 4 */ Higher-Level Functions and Closures import std.stdio; bool delegate(int) gtn(int n) { bool execute(int m) { return m > n; } return &execute; } void main() { auto g5 = gtn(5); // Returns a ">5" delegate writeln(g5(1)); // false writeln(g5(6)); // true } Lambda Expressions auto gtn(int n) { return delegate bool(int m) {return m > n;}; } // Preferred: auto gtn(int n) { return (int m) {return m > n;}; } An Object Calling Environment void main() { class A { int fun() { return 42; } } A a = new A; auto dg = &a.fun; // A “bound method” writeln(dg()); // 42 } Parametric Polymorphism Compile-time Parameters auto gtn(T)(T n) { return (T m) {return m > n;}; } void main() { auto g5 = gtn(5); writeln(g5(1)); writeln(g5(6)); auto g5s = gtn("baz"); writeln(g5s("bar")); writeln(g5s("foo")); } Compile-Time Constraints import std.stdio, std.traits; auto gtn(T)(T n) if (isNumeric!T) { return (T m) {return m > n;}; } void main() { auto g5 = gtn!int(5); writeln(g5(1)); writeln(g5(6)); auto g5s = gtn!string("baz"); // Error writeln(g5s("bar")); writeln(g5s("foo")); } Compile-Time Function Evaluation // ctfe.d: Compile-time function execution import std.stdio, std.conv; int square(int i) { return i * i; } void main() { static int n = square(3); // compile time writefln(text(n)); writefln(text(square(4))); // runtime } Pure Functions For Referential Transparency import std.stdio; import std.conv; pure ulong fib(uint n) { if (n == 0 || n == 1) return n; ulong a = 1, b = 1; foreach (i; 2..n) { auto t = b; b += a; a = t; } return b; } void main(string[] args) { if (args.length > 1) writeln(fib(to!(uint)(args[1]))); } Selected Higher-Level Functions int[] nums = [1,2,3,4,5]; auto squarelist(int[] list) { // 1 4 9 16 25 return map!("a*a")(list); } auto sqsum2(int[] list) { // 55 return reduce!("a + b*b")(0,list); // (list) } auto evens(int[] list) { // 2 4 return filter!("a % 2 == 0")(list); } alias compose!("a/3.0","a*a","a+1.0") comp; writeln(comp(2.0)); // 3 = (2+1)^2 / 3 alias pipe!(div3,sq,pls1) pip; // fns in scope writeln(pip(2.0)); // 1.4444 = (2/3)^2+1 std.functional.memoize Memoization is a technique where a cache of input-output pairs is kept for function calls Avoids recomputing function values See fib.d Variable-length Argument Lists Use “…” in the parameter definition Homogeneous ◦ use “…” in the runtime parameter list ◦ passes a dynamic array Heterogeneous ◦ use “…” in the compile-time parameter list ◦ passes a tuple See varargs.d An Unsafe Function void g() { risky_op1(); // May acquire resources… risky_op2(); risky_op3(); writeln("g succeeded"); } Assume further that these functions must run to completion or not at all. An Unsavory Solution void g() { risky_op1(); try { risky_op2(); } catch (Exception x) { undo_risky_op1(); // Back-out op1 throw x; // Rethrow exception } try { risky_op3(); writeln("g succeeded"); } catch (Exception x) { // Back-out op1 and op2 in reverse order undo_risky_op2(); undo_risky_op1(); throw x; } } D’s scope Statement Options: exit | success | failure void g() { risky_op1(); scope(failure) undo_risky_op1(); risky_op2(); scope(failure) undo_risky_op2(); risky_op3(); writeln("g succeeded"); } Contract Programming Example Also illustrates value types, operator overloading, and unit testing import std.math; // For abs() struct Rational { private int num = 0; private int den = 1; // Local helper function static int gcd(int m, int n) { return n == 0 ? abs(m) : gcd(abs(n), abs(m)%abs(n)); } // Class invariant invariant() { assert(den > 0 && gcd(num, den) == 1); } Contract Programming Example (continued) // Constructor this(int n, int d = 1) // Constructor precondition in { assert(d != 0); } body { num = n; den = d; auto div = gcd(num, den); if (den < 0) div = -div; num /= div; den /= div; } Contract Programming Example (concluded) // ("if" form evaluated at compile time) Rational opBinary(string op)(Rational r) if (op == "+”) { return Rational(num*r.den + den*r.num, den*r.den); } } unittest { // Compile with –unittest option auto r1 = Rational(1,2), r2 = Rational(3,4), r3 = r1 + r2; assert(r3.num == 5 && r3.den == 4); } void main(){} Variant Types Sometimes you want to hold any type ◦ hopefully not too often! Variant can hold any type You can only get out what was last put in ◦ see variant.d Bounded, discriminated unions with Algebraic ◦ like ML unions ◦ see variant2.d Threads A thread is a path of execution inside a running program (process) A thread is launched on a function basis Using the std.concurrency.spawn See concur1.d Message Passing A paradigm for thread-to-thread communication Threads send/receive data to/from each other Less problematic (and less flexible) than sharing data via mutexes See concur1,2,3.d Infinite Streams in D Threads and Message Passing naturally support streams See stream.d