Nimrod logo

Zen of Nim


















Zen of Nim

Uses of Nim

Syntax

Function application

Function application is f(), f(a), f(a, b).

Function application

Function application is f(), f(a), f(a, b).

And here is the sugar:

SugarMeaningExample
1f af(a)spawn log("some message")
2f a, bf(a, b)echo "hello ", "world"
3a.f()f(a)db.fetchRow()
4a.ff(a)mystring.len
5a.f(b)f(a, b)myarray.map(f)
6a.f bf(a, b)db.fetchRow 1
7f"\n"f(r"\n")re"\b[a-z*]\b"
8f a: bf(a, b)lock x: echo "hi"

Function application

Function application is f(), f(a), f(a, b).

And here is the sugar:

SugarMeaningExample
1f af(a)spawn log("some message")
2f a, bf(a, b)echo "hello ", "world"
3a.f()f(a)db.fetchRow()
4a.ff(a)mystring.len
5a.f(b)f(a, b)myarray.map(f)
6a.f bf(a, b)db.fetchRow 1
7f"\n"f(r"\n")re"\b[a-z*]\b"
8f a: bf(a, b)lock x: echo "hi"

BUT: f does not mean f(); myarray.map(f) passes f to map

Operators

1
2
3
4
5
6
7
8
9
func `++`(x: var int; y: int = 1; z: int = 0) =
  x = x + y + z

var g = 70
++g
g ++ 7
# operator in backticks is treated like an 'f':
g.`++`(10, 20)
echo g  # writes 108

Statements vs expressions

Statements require indentation:

# no indentation needed for single assignment statement:
if x: x = false

# indentation needed for nested if statement:
if x:
  if y:
    y = false
else:
  y = true

# indentation needed, because two statements
# follow the condition:
if x:
  x = false
  y = false

Statements vs expressions

Expressions do not:

if thisIsaLongCondition() and
    thisIsAnotherLongCondition(1,
       2, 3, 4):
  x = true

Find first index

func indexOf(s: string; x: set[char]): int =
  for i in 0..<s.len:
    if s[i] in x: return i
  return -1

let whitespacePos = indexOf("abc def", {' ', '\t'})
echo whitespacePos

Zen of Nim


















Concise code is not in conflict with readability, it enables readability.

Compiler must be able to reason about the code

Structured programming

1
2
3
4
5
6
7
8
import tables, strutils

proc countWords(filename: string): CountTable[string] =
  ## Counts all the words in the file.
  result = initCountTable[string]()
  for word in readFile(filename).split:
    result.inc word
  # 'result' instead of 'return', no unstructed control flow

Structured programming

1
2
3
4
for item in collection:
  if item.isBad: continue
  # what do we know here at this point?
  use item

Structured programming

1
2
3
4
5
for item in collection:
  if not item.isBad:
    # what do we know here at this point?
    # that the item is not bad.
    use item

Static typing

distinct & enums & sets

1
2
3
4
5
6
7
8
9
10
type
  SandboxFlag = enum         ## what the interpreter should allow
    allowCast,               ## allow unsafe language feature: 'cast'
    allowFFI,                ## allow the FFI
    allowInfiniteLoops       ## allow endless loops
  
  NimCode = distinct string

proc runNimCode(code: NimCode; flags: set[SandboxFlag] = {allowCast, allowFFI}) =
  ...

Static typing

1
2
3
4
5
6
7
#define allowCast (1 << 0)
#define allowFFI (1 << 1)
#define allowInfiniteLoops (1 << 2)

void runNimCode(char* code, unsigned int flags = allowCast|allowFFI);

runNimCode("4+5", 700); // nobody stops us from passing 700

Static binding

echo "hello ", "world", 99

Static binding

echo "hello ", "world", 99

is rewritten to:

echo([$"hello ", $"world", $99])

Static binding

proc `$`(x: MyObject): string = x.s
var obj = MyObject(s: "xyz")
echo obj  # works

Value based datatypes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type
  Rect = object
    x, y, w, h: int

# construction:
let r = Rect(x: 12, y: 22, w: 40, h: 80)

# field access:
echo r.x, " ", r.y

# assignment does copy:
var other = r
other.x = 10
assert r.x == 12

Side effects tracking

import strutils

proc count(s: string, sub: string): int {.noSideEffect.} =
  result = 0
  var i = 0
  while true:
    i = s.find(sub, i)
    if i < 0: break
    echo "i is: ", i  # error: 'echo' can have side effects
    i += sub.len
    inc result

Side effects tracking

import strutils

proc count(s: string, sub: string): int {.noSideEffect.} =
  result = 0
  var i = 0
  while true:
    i = s.find(sub, i)
    if i < 0: break
    {.cast(noSideEffect).}:
      echo "i is: ", i  # 'cast', so go ahead
    i += sub.len
    inc result

Exception tracking

import os

proc main() {.raises: [].} =
  copyDir("from", "to")
  # Error: copyDir("from", "to") can raise an
  # unlisted exception: ref OSError

Exception tracking

import os

proc main() {.raises: [OSError].} =
  copyDir("from", "to")
  # compiles :-)

Exception tracking

proc x[E]() {.raises: [E].} =
  raise newException(E, "text here")

try:
  x[ValueError]()
except ValueError:
  echo "good"

Mutability restrictions

{.experimental: "strictFuncs".}

type
  Node = ref object
    next, prev: Node
    data: string

func len(n: Node): int =
  var it = n
  result = 0
  while it != nil:
    inc result
    it = it.next

Mutability restrictions

{.experimental: "strictFuncs".}

func insert(x: var seq[Node]; y: Node) =
  let L = x.len
  x.setLen L + 1
  x[L] = y


func doesCompile(n: Node) =
  var m = Node()
  m.data = "abc"

Mutability restrictions

{.experimental: "strictFuncs".}

func doesNotCompile(n: Node) =
  m.data = "abc"

Mutability restrictions

{.experimental: "strictFuncs".}

func select(a, b: Node): Node = b

func mutate(n: Node) =
  var it = n
  let x = it
  let y = x
  let z = y # <-- is the statement that connected
            # the mutation to the parameter
  
  select(x, z).data = "tricky" # <-- the mutation is here
  # Error: an object reachable from 'n'
  # is potentially mutated

Zen of Nim (2)


















If the compiler cannot reason about the code, neither can the programmer.

Copying bad design is not good design

Bad: Language X has feature F, let's have that too!

("C++ has compile-time function evaluation, let's have that too!")

Copying bad design is not good design

Good: We have many use cases for feature F.

("We need to be able to do locking, logging, lazy evaluation,
a typesafe Writeln/Printf, a declarative UI description language,
async and parallel programming! So instead of building these
features into the language, let's have a macro system.")

Meta programming features

Templates for lazy evaluation:

1
2
3
4
5
template log(msg: string) =
  if debug:
    echo msg

log("x: " & $x & ", y: " & $y)

Meta programming features

Roughly comparable to:

1
2
3
4
5
6
#define log(msg) \
  if (debug) { \
    print(msg); \
  }

log("x: " + x.toString() + ", y: " + y.toString());

Meta programming features

Templates for control flow abstraction:

1
2
3
4
5
6
7
8
9
10
template withLock(lock, body) =
  var lock: Lock
  try:
    acquire lock
    body
  finally:
    release lock

withLock myLock:
  accessProtectedResource()

Meta programming features

Macros to implement DSLs:

1
2
3
4
5
6
7
8
html mainPage:
  head:
    title "Zen of Nim"
  body:
    ul:
      li "A bunch of rules that make no sense."

echo mainPage()

Produces:

<html>
  <head><title>Zen of Nim</title></head>
  <body>
    <ul>
      <li>A bunch of rules that make no sense.</li>
    </ul>
  </body>
</html>

Lifting

1
2
3
4
5
6
7
8
9
10
11
12
import math

template liftFromScalar(fname) =
  proc fname[T](x: openArray[T]): seq[T] =
    result = newSeq[typeof(x[0])](x.len)
    for i in 0..<x.len:
      result[i] = fname(x[i])

# make sqrt() work for sequences:
liftFromScalar(sqrt)
echo sqrt(@[4.0, 16.0, 25.0, 36.0])
# => @[2.0, 4.0, 5.0, 6.0]

Declarative programming

1
2
3
4
5
6
7
8
9
10
11
12
13
proc threadTests(r: var Results, cat: Category,
                 options: string) =
  template test(filename: untyped) =
    testSpec r, makeTest("tests/threads" / filename,
      options, cat, actionRun)
    testSpec r, makeTest("tests/threads" / filename,
      options & " -d:release", cat, actionRun)
    testSpec r, makeTest("tests/threads" / filename,
      options & " --tlsEmulation:on", cat, actionRun)
  
  test "tactors"
  test "tactors2"
  test "threadex"

Varargs

test "tactors"
test "tactors2"
test "threadex"

-->

test "tactors", "tactors2", "threadex"

Varargs

1
2
3
4
5
6
7
8
9
import macros

macro apply(caller: untyped;
            args: varargs[untyped]): untyped =
  result = newStmtList()
  for a in args:
    result.add(newCall(caller, a))

apply test, "tactors", "tactors2", "threadex"

Typesafe Writeln/Printf

1
2
3
4
5
6
7
8
9
10
11
12
proc write(f: File; a: int) = echo a
proc write(f: File; a: bool) = echo a
proc write(f: File; a: float) = echo a

proc writeNewline(f: File) =
  echo "\n"

macro writeln*(f: File; args: varargs[typed]) =
  result = newStmtList()
  for a in args:
    result.add newCall(bindSym"write", f, a)
  result.add newCall(bindSym"writeNewline", f)

Don't get in the programmer's way

Emit pragma

1
2
3
4
5
6
7
8
9
10
{.emit: """
static int cvariable = 420;
""".}

proc embedsC() =
  var nimVar = 89
  {.emit: ["""fprintf(stdout, "%d\n", cvariable + (int)""",
    nimVar, ");"].}

embedsC()

Sink parameters

1
2
3
4
func f(x: sink string) =
  discard "do nothing"

f "abc"

Compile with nim c --gc:orc --expandArc:f $file

Sink parameters

1
2
3
func f(x: sink string) =
  discard "do nothing"
  `=destroy`(x)

Nim's intermediate language is Nim itself.

Sink parameters

1
2
3
4
5
6
var g: string

proc f(x: sink string) =
  g = x

f "abc"

Sink parameters

1
2
3
4
5
6
var g: string

proc f(x: sink string) =
  `=sink`(g, x)

f "abc"

Sink parameters

1
2
3
4
5
6
7
8
9
var g: string

proc f(x: sink string) =
  `=sink`(g, x)
  # optimized out:
  wasMoved(x)
  `=destroy`(x)

f "abc"

Destructors

1
2
3
4
5
6
7
8
9
type
  myseq*[T] = object
    len, cap: int
    data: ptr UncheckedArray[T]

proc `=destroy`*[T](x: var myseq[T]) =
  if x.data != nil:
    for i in 0..<x.len: `=destroy`(x[i])
    dealloc(x.data)

Move operator

1
2
3
4
5
6
7
8
proc `=sink`*[T](a: var myseq[T]; b: myseq[T]) =
  # move assignment, optional.
  # Compiler is using `=destroy` and
  # `copyMem` when not provided
  `=destroy`(a)
  a.len = b.len
  a.cap = b.cap
  a.data = b.data

Assignment operator

1
2
3
4
5
6
7
8
9
10
11
proc `=copy`*[T](a: var myseq[T]; b: myseq[T]) =
  # do nothing for self-assignments:
  if a.data == b.data: return
  `=destroy`(a)
  a.len = b.len
  a.cap = b.cap
  if b.data != nil:
    a.data = cast[typeof(a.data)](
      alloc(a.cap * sizeof(T)))
    for i in 0..<a.len:
      a.data[i] = b.data[i]

Accessors

1
2
3
4
5
6
7
8
9
10
11
12
proc add*[T](x: var myseq[T]; y: sink T) =
  if x.len >= x.cap: resize(x)
  x.data[x.len] = y
  inc x.len

proc `[]`*[T](x: myseq[T]; i: Natural): lent T =
  assert i < x.len
  x.data[i]

proc `[]=`*[T](x: var myseq[T]; i: Natural; y: sink T) =
  assert i < x.len
  x.data[i] = y

Zen of Nim


















Customizable memory management

Zen of Nim