C and Nim
A basic object definition in Nim is like a C struct type.
type
Book = object
author: string
title: string
pages: int
var b1: Book # created on the stack
# b1 = nil # ERROR
𝥶If the type is object, then instances of this type will be stored on the stack. Data types stored on the stack can't be nil.
When you create an instance, you can't decide where to store it (on the stack or on the heap). It depends upon the type definition. If it's object, then it's stored on the stack. If it's ref object (see later), then it's stored on the heap.
| C vs Nim
|
typedef char* string;
typedef struct
{
string author;
string title;
int pages;
} Book;
int main()
{
Book b1; // created on the stack
b1.author = "Asimov";
b1.title = "Foundation";
b1.pages = 255;
// initialization:
Book b2 = {"Frank Herbert", "Dune", 412};
return 0;
}
|
type
Book = object
author: string
title: string
pages: int
var b1: Book # created on the stack
b1.author = "Asimov"
b1.title = "Foundation"
b1.pages = 255
echo b1
# (author: "Asimov", title: "Foundation", pages: 255)
let b2 = Book(author: "Frank Herbert", title: "Foundation", pages: 255)
echo b2
# (author: "Frank Herbert", title: "Foundation", pages: 255)
|
𝥶The two examples (C and Nim) are very similar. The variables (b1 and b2) are stored on the stack. It means they are value types. They have value semantics. When you pass it to a procedure, a copy is made. When you use the "=" (assignment) operator, a copy is made.
| value semantics
|
#include <stdio.h>
typedef char *string;
typedef struct
{
string author;
string title;
int pages;
} Book;
void call_sub(Book b)
{
b.pages = 500;
// `b` is a copy; we modify the copy
}
int main()
{
Book b1 = {"Asimov", "Foundation", 255};
printf("pages: %d\n", b1.pages); // 255
call_sub(b1);
printf("pages: %d\n", b1.pages); // 255 (still 255)
Book b3 = b1; // full copy
b3.pages = 500;
printf("b3 pages: %d\n", b3.pages); // 500
printf("b1 pages: %d\n", b1.pages); // 255 (still 255)
return 0;
}
|
type
Book = object
author: string
title: string
pages: int
proc call_sub(b: Book) =
#b.pages = 500 # ERROR: `b` is immutable
# but `b` is a copy of the argument it was called with
discard
var
b1: Book = Book(author: "Asimov", title: "Foundation", pages: 255)
b3: Book
echo "b1 pages: ", b1.pages # 255
call_sub(b1)
b3 = b1 # full copy
b3.pages = 500
echo "b3 pages: ", b3.pages # 500
echo "b1 pages: ", b1.pages # 255 (still 255)
|
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.
Combining the two
When you need both the non-ref and ref types, you can do the following:
type
BookObj = object
author: string
title: string
pages: int
BookRef = ref BookObj # no need to repeat the whole definition
- The convention is to use the
Obj suffix for the non-ref name and Ref for the ref name.
- With
BookRef = ref BookObj you don't have to repeat the whole definition.
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.