See also Part 1: Value semantics
Value types have value semantics, reference types have reference semantics.
The question is how a type behaves when copied or passed around.
Python and Nim
In Python, we have classes. When you make an instance of a class, the instance will be stored on the heap (and thus it's garbage collected). On the stack you have just a reference that points to the object, which is on the heap.
Java does the same thing.
If you want this kind of behaviour, then the type must be ref object (instead of object).
type
# vvv
Book = ref object
author: string
title: string
pages: int
var b: Book
b = nil # allowed, `b` is a reference
| Python vs Nim
|
class Book:
def __init__(self, author, title, pages):
self.author = author
self.title = title
self.pages = pages
def __str__(self):
return f"Book({self.author}, {self.title}, {self.pages})"
def main():
b1 = Book("Asimov", "Foundation", 255)
print(b1) # Book(Asimov, Foundation, 255)
b3 = b1 # b3 and b1 are references
# pointing to the very same object!
b3.pages = 500
print(b3.pages) # 500
print(b1.pages) # 500 (!!!)
if __name__ == "__main__":
main()
|
type
# vvv
Book = ref object
author: string
title: string
pages: int
var
b1 = Book(author: "Asimov", title: "Foundation", pages: 255)
b3 = b1
# b3 and b1 are references
# pointing to the very same object!
b3.pages = 500
echo b3.pages # 500
echo b1.pages # 500 (!!!)
|
𝥶Types defined with the ref keyword are called reference types. When an instance of a reference type is passed to a procedure, then no copy is made of the underlying object. The object is passed by reference.
Pass by reference
In Python, when you create an instance, the object will be stored on the heap, and on the stack you'll have a reference (pointing to the object on the heap). When you pass the object to a procedure, the object will be passed by reference. It means that if you modify the object in the procedure, the call site will see the changes.
In Nim, when you instantiate a ref object, the same thing happens. You get a reference on the stack that points to the object stored on the heap.
Let's see a simplified example. Now the Book type has just one attribute.
| pass by reference
|
class Book:
def __init__(self, pages):
self.pages = pages
def modify(b):
b.pages = 500
def main():
b1 = Book(255)
print(b1.pages) # 255
modify(b1)
print(b1.pages) # 500 (!!!)
if __name__ == "__main__":
main()
|
type
# vvv
BookRef = ref object
pages: int
proc modify(b: BookRef) =
b.pages = 500
var
b1 = BookRef(pages: 255)
echo b1.pages # 255
modify(b1)
echo b1.pages # 500 (!!!)
|
Notice the signature of the Nim proc:
It's not "b: var BookRef", but still, the object on the call site can be modified inside the proc. This proc received the reference of an object (of a ref object), and inside a proc the underlying object can be modified.
In Nim, by default, the formal parameters are read-only. b is also read-only (immutable), but since it's a reference (a pointer), its value is read-only, the memory location it points to. That is, it cannot point to anything else, but the pointed object can change. The reference is immutable, not the pointed object.
Here is a variation of the procedure:
proc modify(b: BookRef) =
let b3 = BookRef(pages: 50)
b.pages = 500 # OK, the pointed object can be modified
# b = b3 # ERROR: `b` cannot be re-assigned
Immutable references are strange
As mentioned earlier, a let reference may act strange. The value of the reference (the memory address it points to) is immutable, not the pointed object.
The reference cannot point to anywhere else. But the underlying object may be modified.
| let references
|
type
# vvv
BookRef = ref object
pages: int
proc modify(b: BookRef) =
b.pages = 500
let
b1 = BookRef(pages: 255)
b3 = BookRef(pages: 50)
echo b1.pages # 255
modify(b1)
echo b1.pages # 500 (!!!)
b1.pages = 1000 # OK
echo b1.pages # 1000 (!!!)
# b1 = b3 # ERROR: `b1` cannot be re-assigned
|
𝥶Here, b1 is a let reference. Later, you can write "b1.pages = 1000", it's fine.
ref object has no default `$`
| toString()
|
type
Book = object
title: string
let b = Book(title: "Foundation")
echo b # (title: "Foundation")
|
type
# vvv
BookRef = ref object
title: string
let b = BookRef(title: "Foundation")
echo b # ERROR: `$` is not defined for this type
|
Solution: if you use ref object, write your own `$` proc:
import std/strformat # &"Hello {name}!"
type
BookRef = ref object
title: string
proc `$`(b: BookRef): string =
&"BookRef(\"{b.title}\")"
let b = BookRef(title: "Foundation")
echo b # BookRef("Foundation")
When to use ref object ?
- recursive data structures (trees, linked lists)
- when you need shared ownership
- when you come from Python/Java and need familiar behavior