Nim Day 2: Hello Gophers! ============================================================ Hey, good news! I have an end goal for this little series: Write a (very) simple Gopher server and Gopher client! Seems appropriate for a phlog, and the Gopher protocol is famously simple, so it's a great beginner project. Today we install Nim, write the traditional "hello world" program, and take a quick tour of the language. Installing Nim ------------------------------------------------------------ First, head on over to Nim's website: https://nim-lang.org/ Navigate to the Install page. (By the way, I'll focus on the UNIX install, but Nim is a first class Windows citizen as well. A binary and instructions are available on the Install page.) As I write this in 2018, Nim provides an installation utility for UNIX-like environments called `choosenim`. If you trust the Nim developers, you can follow the instructions to run the script directly from the 'Net (you do *not* need superuser (root) privileges) and Nim installs itself. Once that's done, you're left with instructions to add the Nim install dir to your $PATH. Here's what I did (replacing <me> with my username): echo 'export PATH=/home/<me>/.nimble/bin:$PATH' >> ~/.bashrc source ~./.bashrc Now you're ready to create a Hello World program. Installing Nim for the paranoid (or security conscious) ------------------------------------------------------------ However, you're too crafty to do something risky like running unknown scripts, right? So you're going to do it like this: $ cd /tmp $ curl https://nim-lang.org/choosenim/init.sh > getnim $ less getnim Now you can review the script before running it. You can confirm for yourself that it does as it claims: # This script performs some platform detection, # downloads the latest version of choosenim and # initiates its installation. (I think one of the most impressive things about this script is seeing the huge number of OS and hardware platforms Nim explicitly supports!) Bad news for the paranoid, though. Turns out that this script's job is to download the binary installer `choosenim` (written in Nim, of course) from Github and run *that*. You can read the source for `choosenim` here: https://github.com/dom96/choosenim But ultimately, you either trust the binary or you don't. So for the truly paranoid, you're going to need to go the source tarball installation route (which is also on the Install page. But are you *really* going to audit all of the source before you compile Nim yourself? At some point, you have to trust something. (Hmmm...this is starting to get philosophical and we have a lot of ground to cover or we'll never get that Gopher server or client written. And that would be sad. So let's assume you've picked the best installation method for you. Me? I trust the Nim devs. Beyond that, I have backups and my secrets are encrypted.) Hello World ------------------------------------------------------------ First, let's make sure you have the compiler successfully installed and in your $PATH (or Windows equivalent) $ nim --version I stick with tradition with new languages: the first thing I write is always a variant of Hello World. So let's type one up and see if we can compile and run it. So fire up Microsoft Word and let's get programming! Actually, if you like, you might want to install a Nim-specific plugin for your favorite editor. (I'm using this with Vim: https://github.com/zah/nim.vim (installed via Vundle)) Create a new file: # hello.nim # As you can tell, these are comments echo "Hello my little gophers!" Compile this treasure: $ nim compile hello.nim Run it: $ ./hello Hello my little gophers! **Woo! Time to celebrate with a big ol' can of beans!** Since we have something to examine, let's take a peek at what was created for us. First, how big is the executable? $ du -h hello 204K hello Ouch! That's pretty big! Ah, but that's a development build with "runtime checks" and no optimizer. We can probably do better with a "release" build: $ nim --help ... --opt:none|speed|size optimize not at all or for speed|size Note: use -d:release for a release build! ... $ nim compile --d:release hello Now how big? $ du -h hello 88K hello Ah, much better. Not that it matters, but can we reduce that even more? $ nim compile --d:release --opt:size hello $ du -h hello 44K hello That'll do. Of course, the equivalent C is smaller: $ cat > chello.c #include <stdio.h> int main(void){ printf("Hello World\n"); return 0; } $ gcc chello.c -o chello $ du -h chello 12K chello But the Nim version has far more "stuff" in its executable. There's a whole Nim standard library in there! Check out the nimcache/ directory which was created for us: $ du -h nimcache/* 4.0K nimcache/hello.c 4.0K nimcache/hello.json 4.0K nimcache/hello.o 148K nimcache/stdlib_system.c 52K nimcache/stdlib_system.o Yeah, that's 148Kb of *stuff* in Nim's included stdlib_system.c. Feel free to read it, but it's largely machine-generated. Same goes for the (much) smaller hello.c from our program. A search for the string "gophers" reveals this delightful line: $ grep gophers nimcache/hello.c STRING_LITERAL(TM_xLHv575t3PG1lB5wK05Xqg_3, "Hello my little gophers!", 24); As your code gets bigger, the Nim standard library does not. It's a fixed, statically-compiled cost, and therefore easy to bear. The wonderful thing is that even as you add more things from Nim libraries, they're also statically compiled into your executable, so it remains just as portable as the equivalent C program. Since we have them, here's some other comparisons between hello and chello (our 'C' hello). Nim hello: $ file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped $ ldd hello linux-vdso.so.1 (0x00007fffc29db000) libdl.so.2 => /lib64/libdl.so.2 (0x00007f523e713000) libc.so.6 => /lib64/libc.so.6 (0x00007f523e34a000) /lib64/ld-linux-x86-64.so.2 (0x0000558719ec9000) C hello: $ file chello chello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped $ ldd chello linux-vdso.so.1 (0x00007ffe4a90e000) libc.so.6 => /lib64/libc.so.6 (0x00007f37efbea000) /lib64/ld-linux-x86-64.so.2 (0x000055bac2c1e000) So the only difference between the executables (besides size) is that Nim dynamically links to `libdl` (for dynamic library functions) from the standard C library. *The point is, Nim writes C so you don't have to!* It can also write JavaScript, C++, and Objective-C. A quick tour of Nim ------------------------------------------------------------ We "ate our vegetables" by diving deep into that mundane Hello World. Now we can enjoy ourselves. There is (for now) a really fun "Easter egg" built into the Nim tool: $ nim secret Hint: used config file '/home/...' [Conf] Hint: system [Processing] Hint: stdin [Processing] >>> 1+1 2 >>> var foo = "A string" >>> echo foo A string >>> Wait!? Nim has a REPL*? Yeah, Nim can run Nim code at compile time as well as generate output source and this "secret" feature lets you treat Nim as an interpreter. (There are some things that aren't available in the secret REPL, but nothing we need yet.) *REPL stands for "Read-Eval-Print Loop" and gives programmers immediate feedback for each statement. They're incredibly handy for learning a new language and just trying things out. So continuing on, we can now use Nim as a calculator: >>> (3+3) * 10 60 Here's a string with escaped (backslashed) quotes: >>> "hello \"world\"" hello "world" Nim has "raw" string literals: >>> r"Raw strings where \ backslashes mean nothing!" Raw strings where \ backslashes mean nothing! And "long" string literals: >>> """Long string literals which ... can even include newlines and "quotes"! ... And slashes \\\ ///""" Long string literals which can even include newlines and "quotes"! And slashes \\\ /// Variable symbols are declared with an explicit type: >>> var x: int >>> x = 5 >>> x 5 Or they can be be inferred at compile time: >>> var x = 5 >>> x 5 There are also constant symbols which must be assigned at compile time and 'let' symbols who can be assigned at runtime, but whose values must never change! We have tuples which are structures with named fields: >>> var gopher = (name: "Ratfactor", location: "Underground") >>> gopher.name Ratfactor >>> gopher.location Underground (That's a tuple literal, you can also declare a variable as having a tuple type.) We have arrays which cannot be resized: >>> var foo = ["a", "b", "c"] >>> foo[0] a >>> foo[0]="x" >>> foo ["x", "b", "c"] >>> foo[3]="z" stack trace: (most recent call last) stdin(53) stdin(53, 4) Error: index out of bounds Error: unhandled exception: index out of bounds [ERecoverableError] And sequences, which can be resized: >>> var foo = @["a", "b", "c"] >>> foo[0] a >>> foo.add("d") >>> foo @["a", "b", "c", "d"] To see if a value is in an array or sequence, you can use `in`: >>> "a" in foo true For control flow, we have a very nice `case` statement which can match multiple values, ranges, and strings. >>> let foo = 3 >>> case foo: ... of 1,2: echo "Too low!" ... of 3: echo "Just right!" ... else: echo "Too high!" ... Just right! Case also evaluates as an expression, so you can do things like this: >>> echo case foo: ... of 1 .. 3: "Okay" ... else: "Not okay" ... Okay If statements are what you'd expect, just remember that Nim has an `elif` statement for "else if": >>> if foo == 2: ... echo "argh!" ... elif foo == 3: ... echo "barg!" ... else: ... echo "narg!" ... barg! While statements are pretty standard: >>> var foo = 5 >>> while foo > 0: ... echo "Hey gophers!" ... dec foo ... Hey gophers! Hey gophers! Hey gophers! Hey gophers! Hey gophers! The for loop is quite nice for iterating using the 'in' keyword: var foo = ["Nim","Is","Cool"] >>> for word in foo: ... echo word ... Nim Is Cool It will also fill an index variable for you: >>> for idx, word in foo: ... echo idx, word ... 0Nim 1Is 2Cool Okay, let's end this little tour with procedures. Here's a silly one that adds 1 to a number: addOne(num: int): int = ... return num+1 ... >>> addOne 7 8 One of my favorite things about Nim procs (and something I've wanted in every other language since I learned about it in Nim) is the `result` variable: >>> proc addOneSometimes(num: int): int = ... result = num + 1 ... if num == 3: ... result = 17 ... >>> addOneSometimes 1 2 >>> addOneSometimes 2 3 >>> addOneSometimes 3 17 See how that works? The variable `result` is implicitly declared for you using the return type of your function. I especially love using `result` when the function requires me to work in stages, creating temporary variable to store possible return values. (I can't think of an exaple off the top of my head just now, but it happens often enough.) ------------------------------------------------------------ Okay, that's quite enough of a taste for now. It's a very pleasant language to read and write and it supports a lot of "modern" conveniences. It has a "script" feel but compiles to fast, native binaries. I think I'll dig into Nim's type system next time. Until then, happy hacking!