📜 ⬆️ ⬇️

What is so special about Nim?



The Nim programming language (previously called Nimrod) is exciting! While the official documentation with examples smoothly introduces the language, I want to quickly show you what can be done with Nim, which would be more difficult or impossible to do in other languages.

I discovered Nim when I was looking for the right tool for writing a game, HoorRace, the successor to my current DDNet game / mod Teeworlds.
')
(note lane. The syntax of Nim was influenced by Modula 3, Delphi, Ada, C ++, Python, Lisp, Oberon.)

Run!


Yes, this part is still not exciting, but just follow the continuation of the post:

for i in 0..10: echo "Hello World"[0..i] 


To run, of course, you will need the Nim compiler (for example, the translation in ArchLinux, for example, the package is community/nim ). Save this code in the file hello.nim, compile it with nim c hello.nim , and finally run the executable file ./hello . Or use the nim -rc hello.nim , which will compile and run the resulting file. To build an optimized version, use the nim -d:release c hello.nim . After launching, you will see this output in the console:

 H He Hel Hell Hello Hello Hello W Hello Wo Hello Wor Hello Worl Hello World 


Execute code at compile time


To implement an efficient CRC32 procedure, you will need a precomputed table. You can calculate it at runtime or save it in the code as a magic array. Of course, we don’t want magic numbers in our code, so we’ll compute a table at the start of the program (at least for now):

 import unsigned, strutils type CRC32* = uint32 const initCRC32* = CRC32(-1) proc createCRCTable(): array[256, CRC32] = for i in 0..255: var rem = CRC32(i) for j in 0..7: if (rem and 1) > 0: rem = (rem shr 1) xor CRC32(0xedb88320) else: rem = rem shr 1 result[i] = rem # Table created at runtime var crc32table = createCRCTable() proc crc32(s): CRC32 = result = initCRC32 for c in s: result = (result shr 8) xor crc32table[(result and 0xff) xor ord(c)] result = not result # String conversion proc $, automatically called by echo proc `$`(c: CRC32): string = int64(c).toHex(8) echo crc32("The quick brown fox jumps over the lazy dog") 


Fine! It works and we got 414FA339 . However, it would be much better if we could calculate the CRC table at compile time. And in Nim this can be done very easily, we replace our string with assignment of crc32table to the following code:

 # Table created at compile time const crc32table = createCRCTable() 

Yes, that's right, all we need to do is replace var with const . Great, isn't it? We can write the same code that can be executed both in the work of the program and at the compilation stage. No template metaprogramming.

Expand language


Templates and macros can be used to avoid copying and noodles in the code, and they will be processed at the compilation stage.

Templates are simply replaced by calls to the corresponding functions at compile time. We can define our own cycles like this:

 template times(x: expr, y: stmt): stmt = for i in 1..x: y 10.times: echo "Hello World" 


The compiler converts times to a normal loop:

 for i in 1..10: echo "Hello World" 


If you are interested in the 10.times syntax, then know that this is just a normal call to times with the first argument 10 and a block of code as the second argument. You could just write: times(10): see more about Unified Call Syntax below.

Or initialize sequences (arrays of arbitrary length) more conveniently:

 template newSeqWith(len: int, init: expr): expr = var result = newSeq[type(init)](len) for i in 0 .. <len: result[i] = init result # Create a 2-dimensional sequence of size 20,10 var seq2D = newSeqWith(20, newSeq[bool](10)) import math randomize() # Create a sequence of 20 random integers smaller than 10 var seqRand = newSeqWith(20, random(10)) echo seqRand 


The macro goes one step further and allows you to analyze and manipulate the AST. For example, in Nim there are no list inclusions (comment of the list comprehensions), but we can add them to the language using a macro. Now instead of:

 var res: seq[int] = @[] for x in 1..10: if x mod 2 == 0: res.add(x) echo res const n = 20 var result: seq[tuple[a,b,c: int]] = @[] for x in 1..n: for y in x..n: for z in y..n: if x*x + y*y == z*z: result.add((x,y,z)) echo result 


You can use the future module and write:

 import future echo lc[x | (x <- 1..10, x mod 2 == 0), int] const n = 20 echo lc[(x,y,z) | (x <- 1..n, y <- x..n, z <- y..n, x*x + y*y == z*z), tuple[a,b,c: int]] 


Add our own optimizations to the compiler


Instead of optimizing your code, would you rather make the compiler smarter? In Nim this is possible!

 var x: int for i in 1..1_000_000_000: x += 2 * i echo x 

This (rather useless) code can be sped up by training the compiler in two optimizations:

 template optMul{`*`(a,2)}(a: int): int = let x = a x + x template canonMul{`*`(a,b)}(a: int{lit}, b: int): int = b * a 

In the first pattern, we indicate that a * 2 can be replaced by a + a . In the second pattern, we indicate that the int variables can be swapped, if the first argument is a constant number, this is necessary so that we can apply the first pattern.

More complex patterns can also be implemented, for example, to optimize Boolean logic:

 template optLog1{a and a}(a): auto = a template optLog2{a and (b or (not b))}(a,b): auto = a template optLog3{a and not a}(a: int): auto = 0 var x = 12 s = x and x # Hint: optLog1(x) --> 'x' [Pattern] r = (x and x) and ((s or s) or (not (s or s))) # Hint: optLog2(x and x, s or s) --> 'x and x' [Pattern] # Hint: optLog1(x) --> 'x' [Pattern] q = (s and not x) and not (s and not x) # Hint: optLog3(s and not x) --> '0' [Pattern] 

Here s optimized to x , r also optimized to x , and q immediately initialized to zero.

If you want to see how templates are used to avoid highlighting bigint , take a look at templates that start with opt in the biginsts.nim library :

 import bigints var i = 0.initBigInt while true: i += 1 echo i 


Connect your C-functions and libraries


Since Nim is translated to C (C ++ / Obj-C), using third-party functions is no problem.

You can easily use your favorite functions from the standard library:

 proc printf(formatstr: cstring) {.header: "<stdio.h>", varargs.} printf("%s %d\n", "foo", 5) 


Or use your own code written in C:

 void hi(char* name) { printf("awesome %s\n", name); } 

 {.compile: "hi.c".} proc hi*(name: cstring) {.importc.} hi "from Nim" 


Or any library you like with c2nim:

 proc set_default_dpi*(dpi: cdouble) {.cdecl, importc: "rsvg_set_default_dpi", dynlib: "librsvg-2.so".} 


Manage the garbage collector


To achieve "soft realtime", you can tell the garbage collector when and how much it can work. The basic logic of the game with the prevention of garbage collector intervention can be implemented on Nim like this:

 gcDisable() while true: gameLogic() renderFrame() gcStep(us = leftTime) sleep(restTime) 


Type safe sets and enum


Often you may need a mathematical set with values ​​that you defined yourself. This is how it can be implemented with the confidence that the types will be checked by the compiler:

 type FakeTune = enum freeze, solo, noJump, noColl, noHook, jetpack var x: set[FakeTune] x.incl freeze x.incl solo x.excl solo echo x + {noColl, noHook} if freeze in x: echo "Here be freeze" var y = {solo, noHook} y.incl 0 # Error: type mismatch 


You cannot accidentally add a value of another type. Internally, it works as an effective bit vector.

The same is possible with arrays, index them with enum.

 var a: array[FakeTune, int] a[freeze] = 100 echo a[freeze] 


Unified Call Syntax


It’s just syntactic sugar, but it’s definitely very convenient (I think it’s terrible!). In Python, I always forget the len and append functions or methods. In Nim, you don't need to remember this, because you can write as you please. Nim uses Unified Call Syntax (Unified Call syntax), which is also now proposed in C ++ by the comrades Herb Sutter and Bjarne Stroustrup.

 var xs = @[1,2,3] # Procedure call syntax add(xs, 4_000_000) echo len(xs) # Method call syntax xs.add(0b0101_0000_0000) echo xs.len() # Command invocation syntax xs.add 0x06_FF_FF_FF echo xs.len 


Performance


(Note: this section in the original article is “outdated”, therefore I offer links to the original updated benchmark and benchmark given in the original article )

From the translator:

In short, Nim generates code that is as fast as human-written C / C ++. Nim can translate the code in C / C ++ / Obj-C (and it will be shown below that it can in JS) and compile it with gcc / clang / llvm_gcc / MS-vcc / Intel-icc. As artificial tests show, Nim is comparable in speed with C / C ++ / D / Rust and faster than Go, Crystal, Java and many others.

We broadcast in JavaScript


Nim can translate Nim code in javascript. This allows you to write both client and server code in Nim. Let's make a small site that will count visitors. This will be our client.nim:

 import htmlgen, dom type Data = object visitors {.importc.}: int uniques {.importc.}: int ip {.importc.}: cstring proc printInfo(data: Data) {.exportc.} = var infoDiv = document.getElementById("info") infoDiv.innerHTML = p("You're visitor number ", $data.visitors, ", unique visitor number ", $data.uniques, " today. Your IP is ", $data.ip, ".") 


We define the type of Data that will be sent from the server to the client. The procedure printInfo will be called with this data for display. To build our client code, run the nim js client command. The result will be saved in nimcache/client.js .

For the server, we will need the Nimble package manager, since we will need to install the Jester (a sinatra-like web framework for Nim). Install Jester: nimble install jester . Now we write our server.nim:

 import jester, asyncdispatch, json, strutils, times, sets, htmlgen, strtabs var visitors = 0 uniques = initSet[string]() time: TimeInfo routes: get "/": resp body( `div`(id="info"), script(src="/client.js", `type`="text/javascript"), script(src="/visitors", `type`="text/javascript")) get "/client.js": const result = staticExec "nim -d:release js client" const clientJS = staticRead "nimcache/client.js" resp clientJS get "/visitors": let newTime = getTime().getLocalTime if newTime.monthDay != time.monthDay: visitors = 0 init uniques time = newTime inc visitors let ip = if request.headers.hasKey "X-Forwarded-For": request.headers["X-Forwarded-For"] else: request.ip uniques.incl ip let json = %{"visitors": %visitors, "uniques": %uniques.len, "ip": %ip} resp "printInfo($#)".format(json) runForever() 


When opening http: // localhost: 5000 /, the server will return a “blank” page with /client.js and /visitors connected. /client.js will return the file received through the nim js client , and /visitors will generate the JS code with a call to printInfo(JSON) .

You can see the resulting Jester site online , it will show this line:

 You're visitor number 11, unique visitor number 11 today. Your IP is 134.90.126.175. 


Conclusion


I hope I could get interested in the Nim programming language.
Please note that the language is not yet completely stable. However, Nim 1.0 is just around the corner. So this is a great time to explore Nim!

Bonus: since Nim is translated to C and depends only on the standard C library, your code will work almost everywhere.

Source: https://habr.com/ru/post/258119/


All Articles