This article is a draft. It is not finished yet and might never be.
Created on 2025/07/21
This is an answer to an article from the developer of the V programming language, which you can find here (archive).
Nim gives a lot of options and freedom to developers. For example, in V you would write foo.bar_baz(), but in Nim all of these are valid: foo.barBaz(), foo.bar_baz(), bar_baz(foo), barBaz(foo), barbaz(foo) etc.
There are two things addressed here: case insensitivity and uniform call syntax.
Programming languages such as C are case sensitive, meaning that uppercase and lower case letters are treated as distinct for indentifiers (for example, getHTTP() and getHttp() are different functions in C). Nim does the opposite, and goes further by treating snake_case and camelCase identifiers as equivalent. Why? Because every user has different writing style, depending on their opinions and programming languages they used beforehand. Best examples are third-party libraries.
Note that the only exception is first uppercase letter, so that you
can still write var item: Item for example. So technically,
Nim is partially case-insensitive.
UFCS is a concept introduced by D, which enables to use function member syntax for regular functions. This has nothing to do with Rust, which misuses this expression instead of qualified path syntax.
While D has methods, Nim does not. It only uses functions (or procedures).
func doubled(n: int): int = n * 2
echo 3.doubled() # 6
This avoids coupling issues and simplifies functions chaining.
let n = doubled(parseInt(readline(stdin)))
# is equivalent to...
let stdin.realine().parseInt().doubled()
# note that parentheses are optional
Try to rewrite this decoding function without using intermediate variables:
func unpadded(line: string): string =
line[0..<line.find(line[^1])]
func decodeToBase32(line: string): string =
let binDigits = line.mapIt(allChars.find(it).toBin(5)).join("")
countup(0, binDigits.len-1, 8)
.toSeq
.mapIt(binDigits[it..<it+8].parseBinInt.char)
.join
.unpadded
In V there’s only one way to return a value from a function: return value. In Nim you can do return value, result = value, value (final expression), or modify a ref argument.
It is very frequent to use a variable to store the result inside a function, and it allows reducing boilerplate, especially with constructors and containers.
func repeatGradually(s: string): string =
var output: string
for i, c in s:
for j in 0..i:
output.add(c)
return output
# is equivalent to ...
func repeatGradually(s: string): string =
for i, c in s:
for j in 0..i:
result.add(c)
# of course it's simplified and you may want to preallocate the string.
No need to remember to return the variable (even though the compiler
warns you when a variable is unused). It’s only annoying when declaring
a proc inside a proc, using a do notation for example.
There is also using value as final expression because it
follows the logic that if/else,
try/except, match can be used as
expressions. Even though you could always use this construction, it
becomes very nested without early returns, which can only be done using
the return keyword.
These three methods have their use and there is no better ones. It’s situational.
However, modifying a variable is generally cheaper than returning a
new copy. To do so you use the var keyword.
func double(s: var string) =
s.add(s)
The ref keyword is used for reference types, and like
Java you can modify them even though they’re immutable.
type Random = ref object
seed: int
current: int
a, c, m: int
func next(self: Random): int =
self.current = (self.a * self.current + self.c) mod self.m
self.current
# Not complete for sake of simplicity
func initRandom(): Random = ...
let rand = initRandom()
echo rand.next
It’s not really equivalent to var parameters though.
Not that you can also modify parameters in V.
struct Point {
mut:
x int
y int
}
fn double(mut p Point) {
p.x = p.x * 2
}
mut p := Point{1, 3}
double(mut p)
println(p)
Features like macros and OOP offer multiple ways to solve problems and increase complexity.
Yes, but macros are for specific use cases and most of the time you don’t need to create them, and if you do, think twice.
For example, even though Nim supports classes and polymorphism (only
for ref objects though), it does not have interfaces or
cascading initialization, yet third-party libraries offer them using
macros.
Another example: when trying to follow a tutorial to build my own lisp, I was stuck
because of a varargs C cleanup function. I wanted to wrap it in a Nim
destructor, but this involves generating a function call
(generating some code => macro).
Unfortunately, since macros are called at compile-time, they cannot use FFI, but these restrictions will be gone with Nimony.
Nim’s strings are mutable, in my opinion this is a huge drawback.