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)
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.
Top comments (1)
"The road is tortuous and the future is bright. We will eventually arrive."
I love your positive thoughts