Caveats

  1. That's not tuple initialization
  2. Default returns
  3. Constant style
  4. Indentation: Nim ain't Python

That's not tuple initialization

What is the output of this program?

let (a, b) = (1, 2)
echo (a, b)

let x, y = (1, 2)
echo (x, y)

It is:

(1, 2)
((1, 2), (1, 2))

The second let does not deconstruct (1, 2), but rather is a shorthand for assigning it to both variables. This is similar to the more familiar var x, y: int syntax that gives two variables the same type.

Nim now has an EachIdentIsTuple warning for the above case. There's no such warning for the following program. In it, are the printed addresses the same?

type
  Odd = object
    id: int

proc `=copy`(dest: var Odd; source: Odd) {.error.}

let x, y = Odd(id: 2)
echo x
echo y
echo cast[int](x.unsafeAddr)
echo cast[int](y.unsafeAddr)

They are not. Sample output:

(id: 2)
(id: 2)
4347016
4347024

This syntax then is not constructing a single object for both variables, but is just a shorthand for the following:

let x = Odd(id: 2)
let y = Odd(id: 2)

For loops have a similar difference between x, y and (x, y):

let a = [(1, 2), (3, 4), (5, 6)]
for x, y in a: echo (x, y)
echo "--"
for (x, y) in a: echo (x, y)

This has the following output, showing that the first loop is binding x to successive indices of the array, and y to the members of the array, whereas the second loop is deconstructing the members of the array:

(0, (1, 2))
(1, (3, 4))
(2, (5, 6))
--
(1, 2)
(3, 4)
(5, 6)

And of course you can do both: for i, (x, y) in a: ...

Default returns

What does the following program print at runtime?

type
  Node = ref object
    case kind: range[0..1]
    of 0: onedata: int
    of 1: twodata: bool

proc get[T](node: Node): T =
  case node.kind
  of 0:
    when T is string: return $node.onedata
    else: assert(false)
  of 1:
    when T is string: return $node.twodata
    else:
      var a: int
      for n in 0 ..< node.kind:
        a.inc n

echo get[seq[int]](Node(kind: 1, twodata: true))

It prints @[], the default value of a seq[int].

With --experimental:strictDefs two warnings are added to the procedure:

proc get[T](node: Node): T =
  case node.kind  # Warning: Cannot prove that 'result' is initialized. This will become a compile time error in the future. [ProveInit]
  of 0:
    when T is string: return $node.onedata
    else: assert(false)
  of 1:
    when T is string: return $node.twodata
    else:
      var a: int
      for n in 0 ..< node.kind:
        a.inc n  # Warning: use explicit initialization of 'a' for clarity [Uninit]

Constant style

Why can't the following program compile?

import std/[net, strformat]

const PORT = 4444

var
  server, client: Socket
  address: string

server = newSocket()
server.setSockOpt(OptReusePort, true)
server.bindAddr(Port(PORT))
server.listen

while true:
  server.accept(client)
  let (address, port) = client.getPeerAddr
  echo &"Client connected from: {address}:{port}"
  client.send "Hello, world!\n"
  client.close

If you don't see it, does the error help?

/path/to/style.nim(11, 21) Error: attempting to call routine: 'Port'
  found 'PORT' [const declared in /path/to/style.nim(3, 7)]
  found nativesockets.Port [type declared in /path/to/Nim/lib/pure/nativ
esockets.nim(59, 3)]

The problem is the constant PORT conflicts with the function Port, because Nim is style insensitive past the first character of an identifier. So this convention of putting constants in all caps, it doesn't really suit Nim, as all that it's done here is inflict on the reader the illusion of the code not having this name conflict.

What other options are there?

option 1: fake namespacing in the style of C symbols

const cfgPort = 4444

This seems to be the route followed in Nim internals and in the stdlib. For File I/O there are fmRead, fmWrite. For networking there's that OptReusePort option.

option 2: inconsistently renaming things when you notice a conflict

const PORT_NO = 4444

This was my first impulse, before I wondered what the point was again of putting this in all caps.

option 3: real namespacing with pure enums?

type
  Config {.pure.} = enum
    Port = 4444
...
server.bindAddr(Port(Config.Port))

This would work for the exact code above, but just by reading through the manual on Enums it should be clear how limiting and annoying this would be in practice. You'd have to reorder your definitions if changing one's value caused it to change its order in the enum, and you're limited to integer values. Also, when even 'pure' enum names don't conflict with another identifier, they'll still be accessible without the Config. prefix:

type
  Example {.pure.} = enum
    Apple = 1
    Orange = 2

let Apple = 10

echo Apple   # output: 10
echo Orange  # not an error. output: Orange

option 4. real namespacing with a constant object

type
  configtype = object
    port: int
    greeting: string

const Config = configtype(
  greeting: "Hello, world!\n",
  port: 4444,
)

This occurred to me later, but it seems like a completely satisfactory solution, and one that lends itself to other uses of the object type.

'Fake namespacing' seems like the best option.

Indentation: Nim ain't Python

What does the following Python program output at runtime?

greeting = "Hello"
    .upper()
    .lower()
print(greeting)

It fails with an error:

  File "dotty.py", line 2
    .upper()
IndentationError: unexpected indent

To continue an complete-looking line onto another line, Python needs the line to look incomplete again, with \ to-be-continued markers.

In Nim though, the equivalent code runs without error:

import strutils

let greeting = "Hello"
  .toUpper
  .toLower
echo greeting