Goodbye Rust, and Hello, D!

After quite a bit of thought and consideration, I have decided to abandon my study of Rust, and move on to D.

I had been learning Rust for a while now, and I have become quite comfortable with it, but there are a few reasons that prompted me to move on to another systems language that would suit me better. Now while D is by no means perfect, I found the following advantages already:

  • A very consistent and intuitive (to me) core language. I have been learning it for only a couple of days now, and I’ve had quite a few “wow, it works exactly as I expected” moments already.
  • Very welcoming and helpful community that actually focuses on the technical side of things rather than getting sidetracked by social causes
  • A wide variety of compilers (DMD, GDC, LDC) targeted for both gcc and LLVM.
  • A stupendously fast compilation cycle. For small programs, I have found
    the compilation times to be on par with Go’s
  • A much saner syntax (in my very early estimation) that actually is quite readable (Rust’s syntax is not quite so readable for anything less-than-trivial in my opinion), and finally
  • Arrays (arguably the most important data structure) are actually sane, consistent, and very much logically intuitive in D unlike the mess that’s C (and C++).
  • Extremely powerful metaprogramming capabilities (an area I am particularly interested in)
  • A much much safer language than C++ while being much more programmer-friendly than Rust. Rust in that sense offloads the whole mental load of memory management onto the developer (and not in a crazy fun way like C or C++).

My use-cases for a systems programming language are quite simple, really – I don’t intend to do OS-level development (at least not in the foreseeable future), and so the GC dependency of D does not affect me in that sense. My research as well as my interactions with the wonderful folks over at forum.dlang.org have convinced me though that even with the GC, performance is still topnotch for a wide variety of systems-level programming.

As part of my learning process (just two days in to be exact), I thought of implementing two programs just to test out the waters before I dived in, and both of them were implemented with much less hassle (infinitely so to be honest), and they just worked beautifully the first time!

Insertion Sort

This is a barebones implementation of Insertion Sort in D:

// insertion_sort.d
import std.stdio;

void insertionSort(int[] arr) {
    foreach(i; 1..arr.length) {
        int key = arr[i];
        int j = cast(int)i-1;
        
        while (j >= 0 && arr[j] > key) {
            arr[j+1] = arr[j];
            j--;
        }
        arr[j+1] = key;
    }
}

void main() {
    int[] array = [100,-199,0,20,2,3,4,5,0,0,1,200];
    
    writefln("Before sorting: %s", array);
    
    insertionSort(array);
    
    writefln("After sorting: %s", array);
}

And here’s a test run:

sh-4.3$ dmd -run insertion_sort.d                                                                                                                                                   
Before sorting: [100, -199, 0, 20, 2, 3, 4, 5, 0, 0, 1, 200]                                                                                                              
After sorting: [-199, 0, 0, 0, 1, 2, 3, 4, 5, 20, 100, 200]  

Simple, and elegant! In fact, this code is directly readable to and understandable by anyone who has even the least bit of familiarity with C, C++, Java, or any member of the extended ALGOL family!

A line number closure

For my second program, I wanted to try out the line number closure (courtesy Hoyte) in D. As it turns out, either D is very logically designed, or my mind is quite attuned to D (or perhaps both!). It was quite pleasurable to be honest. Of course, at this stage, I have no idea if the code is idiomatic D or not. Anyway, let’s have some fun!

// line_closure.d
import std.stdio;

void delegate() getLineClosure() {
    int lineNum = 0;
    
    void printLineNumber() {
        lineNum++;
        
        writefln("Current line: %s", lineNum);
    }
    
    return &printLineNumber;
}

void main() {
    void delegate() x = getLineClosure();
    
    foreach(i; 0..5) {
        x();
    }
    writeln();
    
    void delegate() y = getLineClosure();
    
    foreach(j; 0..3) {
        y();
    }
}

and let’s take it for a spin:

sh-4.3$ dmd -run line_closure.d                                                                                                                                           
Current line: 1                                                                                                                                                           
Current line: 2                                                                                                                                                           
Current line: 3                                                                                                                                                           
Current line: 4                                                                                                                                                           
Current line: 5                                                                                                                                                           
                                                                                                                                                                          
Current line: 1                                                                                                                                                           
Current line: 2                                                                                                                                                           
Current line: 3    

Beautiful! The code probably deserves a bit of explanation – in D, functions are (as far as I can tell), first-class objects, and we can nest functions inside almost any scope. However, a function that is nested within a function or block scope is given a special name – a delegate. So the return type of the getLineClosure function:

void delegate()

indicates that we are returning a delegate which takes no parameters and returns nothing. Also note that we explicitly return the address of the delegate unlike in C, where the name of the function itself is the function pointer.

For this example to work, the delegate has to be able to capture the local variable, lineNum of course, and so delegates in D essentially function as closures in other Functional Programming languages. Also note the clean syntax of foreach for looping (C style loops are also supported, of course).

In conclusion, one major difference that I observed between Rust and D (I’m much more familiar with Rust, of course) is that I can more fully focus on the problem itself with D whereas with Rust, I have to always be aware of (and worrying about) what I am doing with the bindings/variables in my program, and I don’t think it’s altogether a question of getting familiar and comfortable with that. The ownership and borrowing concepts of Rust are, in and of themselves, trivial. However, the entire onus of managing proper memory behaviour is entirely on the developer. I wonder how much that would actually scale in real life. I suppose we’ll know the answer when people start developing large industry-standard (as much as I despise that term) in Rust.

Goodbye Rust, and Hello, D!

Implementing Insertion sort in Rust (and it kinda sucked)

So I’m still learning Rust, and I decided to implement a naive version of the Insertion Sort algorithm as laid out in CLRS. I didn’t realise that it was going to be a bit of an exercise in frustration. Well, first let’s take a look at the code, and then do some analysis on it.

Here’s the C++ version:

#include <iostream>

using namespace std;

void insertion_sort(int arr[], int n)
{
	for (int i = 1; i < n; i++) {
		int key = arr[i];
		int j = i-1;

		while (j >= 0 && (arr[j] > key)) {
			arr[j+1] = arr[j];
			j--;
		}
		arr[j+1] = key;
	}
}

int main()
{
	int arr[] = { 10, -20, 1, 2, 6, 7, 99, 100, 0, 100 };

	insertion_sort(arr, 10);

	for (int& i : arr) {
		cout << i << " ";
	}

	cout << endl;

	return 0;
}

Take it for a spin:

Timmys-MacBook-Pro:my_sort z0ltan$ g++ -Wall -o insertion_sort insertion_sort.cpp -std=c++11
Timmys-MacBook-Pro:my_sort z0ltan$ time ./insertion_sort 
-20 0 1 2 6 7 10 99 100 100 

real	0m0.005s
user	0m0.001s
sys	0m0.003s

Nice! And here’s the Rust version:

pub fn insertion_sort(arr: &mut Vec<i32>) {
	for i in 1..arr.len() {
		
		let key = arr[i];
		let mut t:i32 = (i-1) as i32;
		let mut j = i-1;

		while arr[j] > key {
			arr[j+1] = arr[j];
			
			if t < 0 {
				break;
			}

			if j != 0 {
				j = j-1;
			}

			t = t-1;
		}

		j = (t+1) as usize;
		arr[j] = key;
	}
}


pub fn main() {
	let mut numbers = vec!(10, -20, 1, 2, 6, 7, 99, 100, 0, 100);

	insertion_sort(&mut numbers);

	for num in numbers {
		print!("{} ", num);
	}

	println!("");
}

And here’s the output from a test run:

Timmys-MacBook-Pro:src z0ltan$ rustc insertion_sort.rs
Timmys-MacBook-Pro:src z0ltan$ time ./insertion_sort
-20 0 1 2 6 7 10 99 100 100 

real	0m0.005s
user	0m0.002s
sys	0m0.002s

Great. Now compare the code in the sorting function in C++ and in Rust. The reason why the Rust version is so much more verbose (and filled with ugly casting and checks inside the while loop) is because of the type-safety that Rust provides.

Indexing in C++ and in Java is done using intS, even unsigned ones, and the compilers don’t really enforce any checks on that. So the same code (as in the C++ version) would work fine in Java as well.

In Rust, however, indexing is done using a “unsigned integer pointer size” type called usize. This means that we can’t do something like

   j -= 1;

inside the while loop in the Rust version. My issue is this – the compiler enforces nothing during compile time (aside from warning that a check like j >= 0 would be redundant), and it fails at runtime (as expected). Well, that explains the horrible hack of creating another mutable local variable, t to actually check when j goes 1 below 0 (as is required by this algorithm).

So far, I can’t say that I am overly impressed by Rust. I have seen the compiler miss a lot of clear checks that it could have done. For instance:

x == 1;

when I intended to do

x -= 1;

It shouldn’t be an error, but clearly there can be a warning since it’s basically doing nothing, being a statement than an expression. This may seem like picking at straws, but I wonder if Rust’s safety guarantees aren’t as valuable as the overheads of dealing with the Borrow Checker, and having to massively refactor the code, when business logic changes, to satisfy the Borrow Checker.

So far, what I’ve liked best about Rust is how easy it is to interact with C. C++ support is still far off since Rust does not really have the concept of a class (structS come close), and also name-mangling, but the basic use cases are extremely well-covered and well-designed.

I intend to so some small projects in Rust. Perhaps I will have a better idea of the actual ROI of Rust.

 

Implementing Insertion sort in Rust (and it kinda sucked)