See also Part 2: Reference semantics
Value types have value semantics, reference types have reference semantics.
The question is how a type behaves when copied or passed around.
(1) Primitive types
Primitive types have value semantics.
var
a: int = 3
b: int
b = a # `b` is a copy
echo a # 3
echo b # 3
b = 5 # it has nothing to do with `a`
echo a # 3
echo b # 5
(2) string type
The string data type has value semantics.
var
s: string = "hello"
t: string = s # a copy is made
t[0] = 'X'
echo s # hello
echo t # Xello
Pass it to a proc:
proc add_something(s: string) =
#s.add("!!!") # ERROR: `s` is immutable
discard
var
text = "hello"
echo text # hello
add_something(text) # (passed by value)
echo text # hello
The string has value semantics, so when you pass a string to a proc, in theory, a copy is made of it. However, in practice, the compiler will often optimize away the copy and pass a hidden pointer instead. But this is an optimization of the compiler.
So, in practice, passing a string to a proc is not expensive.
(3) seq type
The seq type is similar to string. It also has value semantics!
var
a: seq[int] = @[1, 2, 3]
b: seq[int] = @[]
b = a # a complete copy is made
echo a # @[1, 2, 3]
echo b # @[1, 2, 3]
b[0] = 99
echo a # @[1, 2, 3]
echo b # @[99, 2, 3]
(4) array type
The array type has value semantics.
var
a: array[3, int] = [1, 2, 3]
b: array[3, int]
b = a # a copy is made
echo a # [1, 2, 3]
echo b # [1, 2, 3]
b[0] = 99
echo a # [1, 2, 3]
echo b # [99, 2, 3]
In C, you cannot assign an array to another array. Here, it works.
(5) object type
The object type also has value semantics!
type
Person = object
name: string
age: int
var
a: Person = Person(name: "Anna", age: 20)
b: Person
b = a # full, independent copy
echo a # (name: "Anna", age: 20)
echo b # (name: "Anna", age: 20)
b.age = 18
echo a.age # 20 (didn't change)
echo b.age # 18
Value types with let enjoy double protection
With a let value type you get both:
- the value itself is immutable — you can't modify the value (content) of the variable
- the variable cannot be re-assigned — you can't "point" it to something else
let
a = 3
b = 5
# a = 20 # ERROR: its value cannot change
# a = b # ERROR: you cannot re-assign the variable
Passing a value type variable to a proc
In Nim, formal parameters are implicitly let variables. What comes from this?
- If you call a proc with a value type, then it is passed by value. Since it is assigned to a
let variable on the formal parameter list, inside the proc that let variable has double protection.
- The compiler can apply tricks behind the scenes to pass arguments cheaply. When you pass a big value type argument, then it won't make a complete copy of it. Instead, it will pass it with a hidden pointer. Thus, even if you pass things by value, it can be a cheap operation.
Example: you pass a seq to a proc.
proc something(li: seq[int]) =
li = @[1, 2, 3] # ERROR: `li` cannot be re-assigned
li[0] = 99 # ERROR: the sequence (content) cannot be changed
# ...
Modifying a value type variable inside a proc
If you want to modify the value type variable on the call site, then you must use var on the formal parameter list:
# vvv
proc modify(x: var int) =
x *= 10
var
a: int = 3
echo a # 3
modify(a)
echo a # 30
Now, in the proc, x is just an alias of the variable the proc was called with. Here, x is an alias of the variable a. When we modify x, we modify a directly on the call site.
However, it only works if the variable on the call site is mutable (var). Otherwise, it can't be changed:
proc modify(x: var int) =
x *= 10
let # !!! it's let this time, not var => immutable
a: int = 3
modify(a) # ERROR: `a` is immutable, cannot be modified
It produces a compile error.
Where are these value type variables stored in the memory?
- a local
int (char, etc.), object, array → stack
- the elements of a
seq or string → heap (always)
- the header of a
seq or string → stack
The compiler doesn't freely choose where to put things. The rules are fixed by the type system.
What the compiler does choose freely is the calling convention — whether to actually copy a value type when passing it to a proc, or to pass a hidden pointer instead as an optimization. But that's about how arguments are passed, not where variables are stored.
So to summarize:
- where things are stored → determined by the type, not the compiler's mood
- how arguments are passed → the compiler may optimize to avoid copies
Python's list vs Nim's seq
The two data structures are very similar. They both are dynamic arrays. The reference (Python) / header (Nim) is on the stack, the elements are on the heap.
However! In Python, the list is an object. And objects in Python are reference types! In Nim, a seq is a value type!
That is: if something is a value type, it doesn't necessarily mean that it's stored on the stack. Nim's seq is implemented the way that the elements are stored on the heap. But, a seq has value semantics.
"value type" ≠ "stored on the stack"
Python's list vs Nim's seq
|
# list has reference semantics
a = [1, 2, 3]
# `a` is a reference, stored on the stack
# [1, 2, 3] is a list object, stored on the heap
# `a` points to the list object
b = a
# `b` points to the same place where `a` points
# both references point to the very same list object
b[0] = 99
print(b) # [99, 2, 3]
print(a) # [99, 2, 3] !!! changed
|
# seq has value semantics
var
a = @[1, 2, 3]
b = a
# `b` is a copy of `a`
b[0] = 99
echo b # @[99, 2, 3]
# the copy is modified
echo a # @[1, 2, 3]
# `a` remains unchanged
|
A value type variable cannot be nil
The special value nil is only valid for reference types in Nim. Value types always hold an actual value, never nil.
type
Book = object
title: string
var
a: int = nil # ERROR
s: string = nil # ERROR
numbers: seq[int] = nil # ERROR
b: array[3, int] = nil # ERROR
o: Book = nil # ERROR
Look at its bright side: you never have to worry about nil checks when working with value types.