EDIT (Jan-13): Use Nim’s -d:release instead of --opt:speed flag. Use Rust’s regex! macro, and collections::HashMap instead of BTreeMap.

EDIT (Jan-15): Add Rust results with regular expression r"[a-zA-Z0-9_]+".

EDIT (Jan-17): Refine Rust code for Zachary Dremann’s pull requests. Edit Rust’s strengths list.

EDIT (Mar-12): Merge Zachary Dremann’s pull request for the newest version of Rust.

Rust and Nim are the two new programming languages I have been following for a while. Shortly after my previous blog post about Rust, Nim 0.10.2 was out. This led me to take a closer look at Nim, and, naturally, compare it with Rust.

In this blog post, I’m going to share my two simple examples written in Nim and Rust, the rough execution time comparison, and my subjective impressions of coding in both.

Example 1: wordcount

This example exercises file I/O, regular expression, hash table (hash map), and command arguments parsing. As the name suggests, it calculates the word counts from the input files or stdin. Here is the usage:

Usage: wordcount [OPTIONS] [FILES]

Options:
    -o:NAME             set output file name
    -i --ignore-case    ignore case
    -h --help           print this help menu

If we pipe the above usage to the program with -i, the output would be:

2       case
1       file
1       files
1       h
2       help
...

Nim version

The Nim version is straightforward. It uses tables.CountTable to count words, parseopt2.getopt to parse command arguments, and sequtils.mapIt for functional mapping operation. For regular expression, I chose pegs which the Nim documentation recommends over re.

The {.raises: [IOError].} compile pragma at line 3 ensures the doWork proc only raises IOError exception. For this, I have to put input.findAll(peg"\w+") inside a try statement at line 21 to contain the exceptions it may theoretically raise.

Here is the code snippet from wordcount.nim:

 1 proc doWork(inFilenames: seq[string] = nil,
 2             outFilename: string = nil,
 3             ignoreCase: bool = false) {.raises: [IOError].} =
 4   # Open files
 5   var
 6     infiles: seq[File] = @[stdin]
 7     outfile: File = stdout
 8   if inFilenames != nil and inFilenames.len > 0:
 9     infiles = inFilenames.mapIt(File, (proc (filename: string): File =
10       if not open(result, filename):
11         raise newException(IOError, "Failed to open file: " & filename)
12     )(it))
13   if outFilename != nil and outFilename.len > 0 and not open(outfile, outFilename, fmWrite):
14     raise newException(IOError, "Failed to open file: " & outFilename)
15 
16   # Parse words
17   var counts = initCountTable[string]()
18   for infile in infiles:
19     for line in infile.lines:
20       let input = if ignoreCase: line.tolower() else: line
21       let words = try: input.findAll(peg"\w+") except: @[]
22       for word in words:
23         counts.inc(word)
24 
25   # Write counts
26   var words = toSeq(counts.keys)
27   sort(words, cmp)
28   for word in words:
29     outfile.writeln(counts[word], '\t', word)

Rust version

To acquaint myself with Rust, I implemented a simple BTreeMap struct akin to collections::BTreeMap, but I ended up with using Rust’s collections::HashMap for fair comparison with Nim. (The code is still left there for your reference.) The getopts crate is used to parse command arguments to my Config struct. Other parts should be straightforward.

Here is the code snippet from my Rust wordcount project:

 1 fn do_work(cfg: &config::Config) -> io::Result<()> {
 2     // Open input and output files
 3     let mut readers = Vec::with_capacity(std::cmp::max(1, cfg.input.len()));
 4     if cfg.input.is_empty() {
 5         readers.push(BufReader::new(Box::new(io::stdin()) as Box<Read>));
 6     } else {
 7         for name in &cfg.input {
 8             let file = try!(File::open(name));
 9             readers.push(BufReader::new(Box::new(file) as Box<Read>));
10         }
11     }
12     let mut writer = match cfg.output {
13         Some(ref name) => {
14             let file = try!(File::create(name));
15             Box::new(BufWriter::new(file)) as Box<Write>
16         }
17         None => { Box::new(io::stdout()) as Box<Write> }
18     };
19 
20     // Parse words
21     let mut map = collections::HashMap::<String, u32>::new();
22     let re = regex!(r"\w+");
23     // let re = regex!(r"[a-zA-Z0-9_]+");
24     for reader in &mut readers {
25         for line in reader.lines() {
26             for caps in re.captures_iter(&line.unwrap()) {
27                 if let Some(cap) = caps.at(0) {
28                     let word = match cfg.ignore_case {
29                         true  => cap.to_ascii_lowercase(),
30                         false => cap.to_string(),
31                     };
32                     match map.entry(word) {
33                         Occupied(mut view) => { *view.get_mut() += 1; }
34                         Vacant(view) => { view.insert(1); }
35                     }
36                 }
37             }
38         }
39     }
40     // Write counts
41     let mut words: Vec<&String> = map.keys().collect();
42     words.sort();
43     for &word in &words {
44         if let Some(count) = map.get(word) {
45             try!(writeln!(writer, "{}\t{}", count, word));
46         }
47     }
48     Ok(())
49 }

Zachary Dremann’s pull request suggested using find_iter. I keep using captures_iter for consistency with the Nim version, but did refine my code a bit.

Execution time comparison

I compiled the code with Nim’s -d:release and Rust’s --release flags. The sample 5MB input file was collected from Nim compiler’s C source files:

$ cat c_code/3_3/*.c > /tmp/input.txt
$ wc /tmp/input.txt
  217898  593776 5503592 /tmp/input.txt

The command to run the Nim version is like this: (Rust’s is similar)

$ time ./wordcount -i -o:result.txt input.txt

Here is the result on my Mac mini with 2.3 GHz Intel Core i7 and 8 GB RAM: (1x = 0.88 seconds)

  Rust regex! \w Regex \w regex! […] Regex […] Nim
release, -i   1x 1.30x 0.44x 1.14x 0.75x
release   1.07x 1.33x 0.50x 1.24x 0.73x
debug, -i   12.65x 20.14x 8.77x 19.42x 3.51x
debug   12.41x 20.09x 8.84x 19.33x 3.25x

Some notes:

  1. Rust regex! runs faster than Regex, and r"[a-zA-Z0-9_]+" faster than r"\w+". All 4 combinations were tested.
  2. The “debug” version is just for your reference.
  3. Nim only ran 1-2% slower with --boundChecks:on, so I didn’t include its result in this example.

Example 2: Conway’s Game of Life

This example runs Conway’s Game of Life on console with a fix-sized map and pattern. (To change the size or pattern, please edit the source code.) It uses ANSI CSI code to redraw the screen.

The output screen looks like:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . (). (). . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . (). . . (). . . . . . . . . . .
. . . . . . . . . . . . . . (). . . . . . . (). . . . . . . . . . . . ()().
. . . . . . . . . . . . . ()()()(). . . . (). . . . (). . . . . . . . ()().
. ()(). . . . . . . . . ()(). (). (). . . . (). . . . . . . . . . . . . . .
. ()(). . . . . . . . ()()(). (). . (). . . (). . . (). . . . . . . . . . .
. . . . . . . . . . . . ()(). (). (). . . . . . (). (). . . . . . . . . . .
. . . . . . . . . . . . . ()()()(). . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . (). . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . (). . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . (). (). . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . ()(). . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . (). . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . (). . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ()()(). . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
n = 300   Press ENTER to exit

The program uses another thread to read stdin, and aborts the game when any line is read.

Nim version

Here is the code snippet from my Nim conway project:

 1 type
 2   Cell = bool
 3   ConwayMap* = array[0.. <mapHeight, array[0.. <mapWidth, Cell]]
 4 
 5 proc init*(map: var ConwayMap, pattern: openarray[string]) =
 6   ## Initialise the map.
 7   let
 8     ix = min(mapWidth, max(@pattern.mapIt(int, it.len)))
 9     iy = min(mapHeight, pattern.len)
10     dx = int((mapWidth - ix) / 2)
11     dy = int((mapHeight - iy) / 2)
12   for y in 0.. <iy:
13     for x in 0.. <ix:
14       if x < pattern[y].len and pattern[y][x] notin Whitespace:
15         map[y + dy][x + dx] = true
16 
17 proc print*(map: ConwayMap) =
18   ## Display the map.
19   ansi.csi(AnsiOp.Clear)
20   ansi.csi(AnsiOp.CursorPos, 1, 1)
21   for row in map:
22     for cell in row:
23       let s = if cell: "()" else: ". "
24       stdout.write(s)
25     stdout.write("\n")
26 
27 proc next*(map: var ConwayMap) =
28   ## Iterate to next state.
29   let oldmap = map
30   for i in 0.. <mapHeight:
31     for j in 0.. <mapWidth:
32       var nlive = 0
33       for i2 in max(i-1, 0)..min(i+1, mapHeight-1):
34         for j2 in max(j-1, 0)..min(j+1, mapWidth-1):
35           if oldmap[i2][j2] and (i2 != i or j2 != j): inc nlive
36       if map[i][j]: map[i][j] = nlive >= 2 and nlive <= 3
37       else: map[i][j] = nlive == 3

Rust version

Here is the code snippet from my Rust conway project:

 1 type Cell = bool;
 2 
 3 #[derive(Copy)]
 4 pub struct Conway {
 5     map: [[Cell; MAP_WIDTH]; MAP_HEIGHT],
 6 }
 7 
 8 impl Conway {
 9     pub fn new() -> Conway {
10         Conway {
11             map: [[false; MAP_WIDTH]; MAP_HEIGHT],
12         }
13     }
14 
15     pub fn init(&mut self, pattern: &[&str]) {
16         let h = pattern.len();
17         let h0 = (MAP_HEIGHT - h) / 2;
18         for i in 0..(h) {
19             let row = pattern[i];
20             let w = row.len();
21             let w0 = (MAP_WIDTH - w) / 2;
22             for (j, c) in row.chars().enumerate() {
23                 self.map[i + h0][j + w0] = c == '1';
24             }
25         }
26     }
27 
28     /// Iterate to next state. Return false if the state remains unchanged.
29     pub fn next(&mut self) -> bool {
30         let mut newmap = [[false; MAP_WIDTH]; MAP_HEIGHT];
31         for i in 0..(MAP_HEIGHT) {
32             for j in 0..(MAP_WIDTH) {
33                 let mut nlive = 0;
34                 for i2 in i.saturating_sub(1)..cmp::min(i+2, MAP_HEIGHT) {
35                     for j2 in j.saturating_sub(1)..cmp::min(j+2, MAP_WIDTH) {
36                         if self.map[i2][j2] && (i2 != i || j2 != j) {
37                             nlive += 1;
38                         }
39                     }
40                 }
41                 newmap[i][j] = match (self.map[i][j], nlive) {
42                     (true, 2) | (true, 3) => true,
43                     (true, _) => false,
44                     (false, 3) => true,
45                     (false, _) => false,
46                 };
47             }
48         }
49         // let changed = self.map != newmap;
50         let changed = true;
51         self.map = newmap;
52         changed
53     }
54 }
55 
56 impl fmt::Display for Conway {
57     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58         for row in self.map.iter() {
59             for cell in row.iter() {
60                 try!(write!(f, "{}", if *cell { "()" } else { ". " }));
61             }
62             try!(write!(f, "\n"));
63         }
64         Ok(())
65     }
66 }

In line 49, I intended to determine if the new map has changed, but it turns out a simple comparison self.map != newmap won’t work for array size > 32, unless you implement PartialEq trait.

Note that using unsafe libc::exit in my main.rs is very non-idiomatic in Rust. Zachary Dremann’s pull request elegantly avoided the glaring libc::exit with select! and a non-blocking timer receiver. You may want to take a look.

Execution time comparison

To measure the execution time, some code changes are needed:

  1. Comment out the sleep function in conway.nim and main.rs.
  2. Change the loop count from 300 to 30000.
  3. As the map redraw is time-consuming, the measurement is taken both (1) with and (2) without map print (i.e. comment out the map print lines in conway.nim and main.rs).

Here is the result with Nim’s -d:release and Rust’s --release flags:

  Rust Nim Nim/bc:on n=30000
(1) with map print 1x 1.75x 1.87x 1x=3.33s
(2) without map print 1x 1.15x 1.72x 1x=0.78s

(Since Rust does bounds checking, to be fair, I added a column “Nim/bc:on” for Nim binaries compiled with an additional --boundChecks:on flag.)

Nim vs. Rust

Although Nim and Rust are both compiled languages aiming for good performance, they are two very different languages. Their limited similarities I can think up include:

  • compiled and statically typed
  • aiming for good performance (either could run faster depending on the underlying implementations, and further optimizations are probable)
  • composition over inheritance (this seems a trend of new languages?)
  • easy C binding
  • popular language goodies: generics, closure, functional features, type inference, macro, statement as expression…

But their differences are more interesting.

Philosophy: freedom vs. discipline

Coding in Nim often gives me an illusion of scripting languages. It really blurs the line. Nim tries to remove the coding friction as much as possible, and writing in Nim is a great joy.

However, there is a flip side when leaning toward the writer’s freedom too much: the explicitness, clarity or even maintainability could be hampered. Let me give a small example: in Nim, import will bring in all module symbols to your namespace. A symbol of a module can be qualified with module.symbol syntax, or you can use from module import nil to force qualification, but really, who will bother doing this? And this seems not “idiomatic Nim” anyway. The result is, you often cannot tell which symbol comes from which module when reading others’ (or your) code. (Fortunately, there won’t be naming collision bugs because Nim compiler forces you to qualify in such cases.)

Other examples include: UFCS that lets you use len(x), len x, x.len() or x.len freely, whichever you like; underline- and case-insensitive, so mapWidth, mapwidth and map_width all map to the same name (I’m glad they enabled the “partial case sensitivity” rule in 0.10.2, therefore Foo and foo at least differ); use of uninitialised values is OK; etc. In theory, you can follow strict style guidelines to mitigate the issue, but you tend to be more relaxed when coding in Nim.

On the other hand, Rust honors discipline. Its compiler is very rigid. Everything must be crystal clear. You have to get a lot of things right beforehand. Ambiguity is not an attribute of Rust code… Things like these are usually good for long-lived projects and maintainability, but coding in Rust may feel restrictive and force you to take care of some details you’re not interested of. You are also tempted into memory or performance optimisation even it’s not your priority. You tend to be more disciplined when coding in Rust.

Both have their pros and cons. As a coder, I enjoy coding in Nim more; as a maintainer, I would rather maintain products written in Rust.

Visual style: Python-ish vs. C++-ish

Like Python, Nim uses indentation to delimit blocks and has less sigils. Rust is more like C++. Those {}, ::, <> and & should be familiar to C++ programmers, plus some new ones like 'a.

Occasionally, Nim can be too literal. For example, I think Rust’s match syntax:

match key.cmp(&node.key) {
    Less    => return insert(&mut node.left, key, value),
    Greater => return insert(&mut node.right, key, value),
    Equal   => node.value = value,
}

looks clearer than Nim’s case statement:

case key
of "help", "h": echo usageString
of "ignore-case", "i": ignoreCase = true
of "o": outFilename = val
else: discard

But overall, Nim code is less visually noisy. Rust lifetime parameters are particularly cluttering IMO and, unfortunately, unlikely to change since it has passed 1.0 Alpha.

Memory management: GC vs. manual safety

Though Nim allows unsafe (untraced) memory management and provides soft realtime support with more predictable GC behaviour, it’s still a GC language, having all the pros and cons of GC. Nim’s object assignment is value copy. If you can live with GC, Nim memory management should be effortless to you.

Rust provides limited GC support, but more often than not, you’ll rely on Rust’s ownership system for memory management. The abstraction is so thin that you are basically managing the memory on your own. As a Rust programmer, you need to fully comprehend its memory management model (ownership, borrow and lifetimes) before you can code in Rust effectively, which is often the first barrier inflicting Rust newcomers.

On the other hand, it’s Rust’s most unique strength: safe memory management without GC. Rust solved this tough challenge beautifully. This along with resource safety, concurrent data safety and the elimination of null pointers makes Rust an extremely safe, reliable, low (or even no) runtime-overhead programming language.

Depending on your requirements, Nim’s GC can be good enough, or Rust is your only sensible choice.

Other differences

Nim’s strengths:

  • Productivity: within the same unit of time, you can crank out more features in Nim
  • Ease of learning
  • Scripting in a compiled language, good for prototyping, interactive exploration, batch processing etc.
  • Language features:
    • method overloading
    • new operator definition
    • named arguments and default values
    • powerful compile-time coding

Rust’s strengths:

  • A true systems programming language: embeddable, GC-free and bare metal
  • Safety, correctness, runtime reliability
  • Strong core team and vibrant community
  • Language features:
    • pattern matching (excellent!)
    • enum variants (though Nim’s object variants are good, too)
    • let mut instead of var (small thing but matters)
    • powerful destructuring syntax

Error handling: Nim opts for the common exception mechanism while Rust uses a flexible Result type (and panic!). I have no strong preference here, but consider it an important difference worth mentioning.

Release 1.0 is on the way

Both Nim and Rust target 1.0 release at the first half of this year. That’s very exciting! Rust has gained a lot of attention and Nim is becoming better-known, too. Their flavors are very different, but both are great new programming languages. Rust shines when safety, performance or reliability matters. Nim is nimble, expressive, blending the strengths of scripting languages and compiled languages well. They should be great additions to your language selection.

Hope this article has given you a glimpse on them.