This article is a draft. It is not finished yet and might never be.
Created on 07/21/2025
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 concepts shown here for Nim: case insensivity and uniform call syntax.
Case insensivity in Nim forces to find better variables and function names, and although you could call barBar(foo)
and bar_baz(foo)
, this shouldn’t prevent you from having a consistent code style. It is actually very helpful when using functions from other libraries, as you don’t need to worry about the case of the variables, functions, and types you will be using.
Quoting from Nim’s wiki:
Why is it case/underscore insensitive?
- Identifiers which only differ in case are bad style. If the programming language treats them the same the programmer needs to come up with different names for different things.
- Case insensitivity is widely considered to be more user friendly. This holds for file systems, configuration files, and programming languages.
- It allows for using a consistent style in your own codebase, regardless of whether upstream libraries use camelCase, snake_case, or a mix.
- Note that most people confuse case sensitivity with case consistency (which is indeed good style). However, case consistency is easier to achieve with case insensitivity and a properly configured IDE than with case sensitivity.
- Many programming languages are case insensitive: Lisp, Basic, Pascal, Ada, Eiffel, Fortran. Since software for aircrafts and power plants has been written in Ada, it seems reasonable to assume that case insensitivity will not destroy civilization.
- It prevents bugs: in large applications in other languages it’s not uncommon to see bugs introduced by an incorrect completion, e.g. updatePlayerstatus / updatePlayerStatus / update_player_status. With case/underscore insensitivity you know in advance that there can be only one “updateplayerstatus” in your code (and write it in a consistent manner, e.g. always update_player_status)
Basically, UFCS is a feature introduced by D that enables to call a function in two ways:
ufcs.nim# Like this
var myArray = [1, 2, 3]
echo len(myArray)
# Or like this
# Parentheses are optional here, you can write myArray.len
echo myArray.len()
No method here, len
is simply a function.
The only thing I dislike in Nim is that it is too permissive: parentheses are optional when the function only has one argument, and parentheses are optional in general (as you can see with echo).
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 aref
argument.
result = value
This one is easier to reason about when dealing with strings and seqs.
Tell me what’s best between :
proc repeatGradually(s: string): string =
var output: string
for i, c in s:
for j in 0..i:
output.add(c)
return output
…and…
proc repeatGradually(s: string): string =
for i, c in s:
for j in 0..i:
result.add(c)
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.
Unlike V, Nim generates unreadable C code with lots of extra bloat.
In Nim, the generated C code is for machine consumption, and not human consumption. The author then shows C output when compiling a program in %%release%% mode. Release mode enables optimizations for a faster program, at the cost of a longer compilation. So to read the generated code in the best way possible, the program should have been compiled in %%debug%% mode.
Let’s check the generated code in debug mode then. First off, the User type is not defined here, so the code to test should be this one:
type User = object
name: string
last_name: string
age: Natural
var users = [
User(name: "Carl", last_name: "Black", age: 22),
User(name: "Sam", last_name: "Johnson", age: 23)
]
I’m using this file structure btw:
letest (directory)
|----- output (directory)
|----- testus.nim (file)
Then I run the following command: nim c --nimcache:output testus.nim
We get this (I’m only showing the important part):
@mtestus.nim.cstruct tyObject_User__yIweK7IYMHUbiRy0ABSYvA {
NimStringV2 name;
NimStringV2 last_name;
NI age;
};
static const struct {
NI cap; NIM_CHAR data[4+1];
} TM__YPEKt9akeoqMQZ8Cz6qGk4w_3 = { 4 | NIM_STRLIT_FLAG, "Carl" };
static const struct {
NI cap; NIM_CHAR data[5+1];
} TM__YPEKt9akeoqMQZ8Cz6qGk4w_4 = { 5 | NIM_STRLIT_FLAG, "Black" };
static const struct {
NI cap; NIM_CHAR data[3+1];
} TM__YPEKt9akeoqMQZ8Cz6qGk4w_5 = { 3 | NIM_STRLIT_FLAG, "Sam" };
static const struct {
NI cap; NIM_CHAR data[7+1];
} TM__YPEKt9akeoqMQZ8Cz6qGk4w_6 = { 7 | NIM_STRLIT_FLAG, "Johnson" };
static NIM_CONST tyArray__otJc6muQBkFAOAJg7zYB1Q TM__YPEKt9akeoqMQZ8Cz6qGk4w_2 = {{{4, (NimStrPayload*)&TM__YPEKt9akeoqMQZ8Cz6qGk4w_3}, {5, (NimStrPayload*)&TM__YPEKt9akeoqMQZ8Cz6qGk4w_4}, ((NI)22)}
,
{{3, (NimStrPayload*)&TM__YPEKt9akeoqMQZ8Cz6qGk4w_5}, {7, (NimStrPayload*)&TM__YPEKt9akeoqMQZ8Cz6qGk4w_6}, ((NI)23)}
}
;
Again, it is not readable, but it is made for computers. The extra bloat code may be due to debug information.
Note that even in danger mode (the fastest release possible), this structure is kept, only the main and memory allocations logics vary.