loading...
Cover image for Sweet, sweet, sweeeeeet Nim

Sweet, sweet, sweeeeeet Nim

xflywind profile image flywind ・7 min read

Can you smell sweet air when you use Nim language ? Yes, I can! Because Nim has excessive sugar :).
Why Nim is so sweet? Because Nim has a powerful macro system which allows direct manipulation of the AST, offering nearly unlimited opportunities. This is usually called syntax sugar or DSL which reduces the complexity of programming.

Requirements

Your Nim version is at least more than 1.2.0.

with

Are you tired of writing the ugly chaining of function calls?

Put them in one line.

Well, if I can afford a 24-inch display, I would like this.

uglyChaining.add(substr = "hello").strip(leading = false).clear(total = true)

Split them in multi-lines

Emm, it looks better. But where should I align my dots? This will cause a war just like where braces should be placed in unless you have a better ide :P.

uglyChaining.add(substr = "hello")
            .strip(leading = false)
            .clear(total = true)

uglyChaining.add(substr = "hello").
             strip(leading = false).
             clear(total = true)

Nim saves you from this

Using var means it is mutable, Now we have a unified way to do that.

# example.nim
import std/with


var x = "Hello, "

with x:
  add "Nim!\n"
  add "It's an elegant language.\n"
  add "Welcome to this beautiful world!"

What does with do? Nim provides a hands-on command to expand macros:

Type nim c -r --expandMacro:with example.nim, here are results:

example.nim(9, 6) Hint: expanded macro:
add x, "Nim!\n"
add x, "It\'s an elegant language.\n"
add x, "Welcome to this beautiful world!" [ExpandMacro]

Yes, they are transformed to plain function calls. But you just write DSLs without knowing its internal implementations.

Of course, there are some requirements you should obey:

The type of first parameter must be in accordance with x .

proc addTwoInts*(s: var string, a, b: int) =
  s.addInt a
  s.addInt b

proc addStatic*(s: var string) =
  s.add "static"

proc addSideEffect*(s: string) =
  echo s

These three functions are all allowed. There will be automatically expanded to:

example.nim(18, 6) Hint: expanded macro:
add x, "Nim!\n"
add x, "It\'s an elegant language.\n"
add x, "Welcome to this beautiful world!"
addTwoInts x, 12, 3
addStatic(x)
addSideEffect(x) [ExpandMacro]

So far so good, let’s look at the next feature.

dup

When I wrote some function declarations, I was lost in thought: “To be or not to be, this is a question”. Oh no, the Nim problems were too hard to deal with and caused me distract. The real problem is whether we should implement inplace version or outplace version instead. As I am a mature adult, I usually want both of them.

Inplace version and outplace version have their own advantages and disadvantages. Inplace version is more efficient in most situations.

proc addInplace(a: var string, b: string) =
  a.add b

proc addoutplace(a: string, b: string): string =
  a & b

In my computer, these two functions have big difference in performance.

First we write a simple test program for inplace version and use command nim c –d:release test_inplace.nim to compile program:

proc addInplace(a: var string, b: string) =
  a.add b


var s: string

for i in 1 .. 25:
  s.addInplace($1)

echo s[0]

Type time ./test_inplace

1

real    0m0.001s
user    0m0.001s
sys     0m0.000s

Then we write test program for outplace version and use command nim c –d:release test_outplace.nim to compile program:

proc addOutplace(a: string, b: string): string =
  a & b

var s: string

for i in 1 .. 25:
  s.add addOutplace(s, $i)

echo s[0]

Type time ./test_outplace

1

real    0m0.084s
user    0m0.020s
sys     0m0.064s

Yes, inplace version is really fast. However, sometimes we don’t want to modify original string, we need to generate a new string. Now outplace version comes to our rescue. When we design our APIs, we may have a bunch of questions. Which one should I write? Will my users will need another version of functions? Or should I write both of them for my function call? And If I write one two-version function call, should I make the rest of function call become two-version?

Thanks to god! I met Nim! we have dup to reduce redundancy. Now we only need to implement inplace version.

Take this inplace function for example:

import sugar

proc addInplace(a: var string, b: string) =
  a.add b

var a = "Hello, "
doAssert a.dup(addInplace("Nim!")) == "Hello, Nim!"
doAssert dup(a, addInplace("Nim!")) == "Hello, Nim!"

You may want to ask, is this also the “magic” of macros? Bingo. After all, we all knows that

The best way to learn macros is to expand macros. —— myself :)

Now we know the macros are just paper tigers. They sound difficult and dreadful. But when you expand them, they are merely plain Nim statements or plain functions. Macros just construct plain Nim statement. But if you can grasp macros, you can say goodbye to duplicated codes.

Now we reduce temporary variable and make it one line.

example.nim(7, 11) Hint: expanded macro:
var dupResult_14505006 = a
addInplace(dupResult_14505006, "Nim!")
dupResult_14505006 [ExpandMacro]

It also allows multiple inplace function calls.

doAssert a.dup(addInplace("Nim:)"), removeSuffix(":)")) == "Hello, Nim"
doAssert dup(a, addInplace("Nim:)"), removeSuffix(":)")) == "Hello, Nim"

collect

We may know that Python has list comprehensions. When we don‘t have too much nested structure, list comprehension is really elegant to use.

Nim language introduces collect macros to implement list, tables, set comprehensions. Different from Python, it is in the indentation forms. Even you have much nested and complex logic, it is still clear to use.

import sugar


let origin = [1, 2, 3, 4, 5]
let extra = [3, 5, 7, 9, 11]

let data = collect(newSeq):
  for idx in 0 ..< origin.len:
    let a = origin[idx]
    let b = extra[idx]
    if (a + b) mod 2 == 0:
      b - a

doassert data == @[2, 4, 6]

Let’s expand macros first. It just adds elements as usual. However collect macros encapsulate all needed block and do not introduce unnecessary temporary variables. It is more clear to show what we want to do.

example.nim(7, 19) Hint: expanded macro:
var collectResult_6206028 = newSeq(Natural(0))
for idx in 0 ..< len(origin):
  let a = origin[idx]
  let b = extra[idx]
  if (a + b) mod 2 == 0:
    add(collectResult_6206028, b - a)
collectResult_6206028

debug-string

Sometimes, we want to use echo to debug programs. However, echo introduces many duplicates. I don’t want to write:

let language = "Nim"
echo "language = ", language
# Output: language = Nim

language = is totally duplicated since I can get its name in compile time. That’s what dump does:

import sugar

let language = "Nim"
dump(language)
# Output: language = Nim

Sometimes, dump can’t satisfy our needs. For example, we want more verbose information such as filename and line info. We can extend dump by writing our own debug macros.

Let’s do it.

# import necessary modules
import macros, sugar


macro dumpVerbose*(s: untyped): untyped =
  let info = s.lineInfo # get line infos
  result = quote do:
    stdout.write `info` & "|>"
    dump(`s`)

# output:
# example.nim(15, 14)|> a + 12 = 26

Wow, our dumpVerbose is just five lines, but it can print its line info and filename. Excellent!

What if we want to print multiple error messages without introducing duplicates? Now Nim implements a new format debug string which reduces many duplicates. However it is in development. Make sure you install the latest Nim devel version(At least newer than 2020-07-01:

nim -v
Nim Compiler Version 1.3.5 [Windows: amd64]
Compiled at 2020-07-01
Copyright (c) 2006-2020 by Andreas Rumpf

Let’s start our journey:

import strformat


let language = "Nim"
echo fmt"{language=} {language=} {language=}"
# Output: language=Nim language=Nim language=Nim

Isn't it really difficult? Be careful. It is space sensitive.

import strformat


proc hello(a: string, b: float): int = 12

let a = "hello"
let b = 3.1415926
doAssert fmt"{hello(x, y) = }" == "hello(x, y) = 12"
doAssert fmt"{x.hello(y) = }" == "x.hello(y) = 12"
doAssert fmt"{hello x, y = }" == "hello x, y = 12"

Karax

Finally Let’s look at how elegant macros are. Karax is a framework for developing single page applications in Nim. You can also use Karax for server side HTML rendering. Instead of writing endless braces, you can use Nim syntax to write HTML. It’s more powerful. You can easily abstract multiple components and combine them together.

import karax / [karaxdsl, vdom]

const languages = @["Nim", "Python", "C++", "Java"]

proc render*(): string =
  let vnode = buildHtml(tdiv(class = "mt-3")):
    h1: text "Which is your favourite programming language?"
    p: text "echo Hello world"
    ul:
      for language in languages:
        li: text language
    dl:
      dt: text "Can I use Karax for client side single page apps?"
      dd: text "Yes"

      dt: text "Can I use Karax for server side HTML rendering?"
      dd: text "Yes"
  result = $vnode

echo render()

output:

<div class="mt-3">
  <h1>Which is your favourite programming language?</h1>
  <p>echo Hello Nim</p>
  <ul>
    <li>Nim</li>
    <li>Python</li>
    <li>C++</li>
    <li>Java</li>
  </ul>
  <dl>
    <dt>Can I use Karax for client side single page apps?</dt>
    <dd>Yes</dd>
    <dt>Can I use Karax for server side HTML rendering?</dt>
    <dd>Yes</dd>
  </dl>
</div>

Now let’s open it on the browser:

import browsers


let filename = "example.html"
writeFile(filename, render())
openDefaultBrowser(filename)

Alt Text

That’s it. Although Nim is still young and sometimes bugs drive me crazy, It has promising futures. In the first sight, you may find it wired(I’m different, in the first sight I think it is elegant :P). Write some simple programs and you will find whether Nim is suitable for you.

The road is tortuous and the future is bright. We will eventually arrive.

Discussion

markdown guide