Comparing V and Nim

This article is a draft. It is not finished yet and might never be.

Back to articles

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.

Case insensitivity

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.

Uniform function call syntax (UFCS)

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.