Let's Explore proc and method in Nim
When starting out with Nim, I came across what appeared to be a frustrating little bug. I had a series of objects which inherited from a parent level object. There were overloaded procedures that were called on each of these objects, and a fallback procedure for the parent object. (Sample code below.) However, every time I called the proc, it would get called on the parent object rather than the inherited type.
As you might expect, this was due to my misunderstanding of the language rather than a bug with Nim! This post explores some differences in how Nim runs procedures and methods. (Start here if you're new to object-oriented programming with Nim.)
I am still relatively new to Nim, and I don't have a computer science background, so it's important to warn you that this is almost certainly not a scientific or collegiate level discussion on the topic. Instead, it's meant to give a practical overview of my understanding in easy to comprehend terms.
Procedure and Methods and Functions, Oh My!
On some technical, scholarly level there is no doubt a difference between a method, a function, a procedure, and a subroutine. (Did I miss any?) Since each programming language seems to use the terminology differently, and in conversation they are used interchangeably, all such distinction has largely been lost.
For the most part, Nim uses what it calls procedures, using the proc
keyword. It seems to be one of an elite few languages to use this keyword, while others might use function
or func
or method
or def
or even sub
. It is what Nim uses throughout the majority of its documentation and its tutorials.
Then you get halfway through its second tutorial and it briefly throws a method
at you. It's in a section all about static vs dynamic dispatch, which is one of those computer science-y terms that most programmers who deal with high-level languages probably have never heard of. It certainly sounded familiar to me, but it was not something I had ever explored in any way before and could not explain if my life depended on it. (Other than the fact that, you know, one gets dispatched statically, and the other happens... dynamically.)
I'm going to start by linking to a great, easy to understand reference on dynamic vs. static dispatching. Lukas certainly manages to explain it more in-depth than I could hope for, and it's relatively easy to understand. I'm more concerned about how it affects Nim specifically, and when and why you should use each one.
Nim Cheats at Object-Oriented Programming
This is important to understand. While most languages have strong OO capabilities (Python), or go completely crazy with objects (Java), Nim merely fakes it. It has basic object types, which have attributes, and a single level of inheritance, and that's it. Since the language is statically typed, procedures can be written specifically for a particular object type, which sort of mimics a class method. Combine that with the method-call syntax and you have something approaching object-oriented programming.
This is best demonstrated with a example code. Here is the beginnings of a note application that has text notes, reminder notes, and task notes.
import times
type
Note = ref object of RootObj
text: string
TaskNote = ref object of Note
completed: bool
ReminderNote = ref object of Note
due_timestamp: DateTime
proc render(note: Note): string =
return note.text
proc render(note: TaskNote): string =
case note.completed
of true:
return "☑ " & note.text
of false:
return "☐ " & note.text
proc render(note: ReminderNote): string =
return note.due_timestamp.format("yyyy-MM-dd") & " " & note.text
let textNote = Note(text: "Just a plain note")
let taskNote = TaskNote(text: "Do me soon!", completed: false)
let reminderNote = ReminderNote(text: "Don't forget about me...", due_timestamp: now() + 1.months)
echo textNote.render() # same as calling render(textNote)
echo taskNote.render() # same as calling render(taskNote)
echo reminderNote.render() # same as calling render(reminderNote)
# output:
# Just a plain note
# ☐ Do me soon!
# 2019-04-19 Don't forget about me...
Note that the render methods are not actually attached to the objects. They use Nim's method call syntax.
This works exactly as expected so far. Great!
Now, Nim lets us handle objects pretty loosely. In this case, any instance that is a ReminderNote is also an instance of Note and can be treated as such without extra work on our part. (In fact, if we didn't have proc render(note: ReminderNote): string =
procedure in our code, a ReminderNote would get rendered using proc render(note: Note): string =
. Try it!)
If we wanted our notes to have a parent note, that could be of any type, it's easy to add. Since each type of note could have a parent we would add it as a property on our root Note type. (Nim also allows us to use self-referential objects.) The parent note is also of the type Note. This allows us to link to any instance that is of type Note, regardless of whether it is just a Note object or if it inherits from the Note object. In other words, the parent could be type Note, TaskNote, or ReminderNote.
import times
type
Note = ref object of RootObj
text: string
parent: Note
TaskNote = ref object of Note
completed: bool
ReminderNote = ref object of Note
due_timestamp: DateTime
proc render(note: Note): string =
return note.text
proc render(note: TaskNote): string =
case note.completed
of true:
return "☑ " & note.text
of false:
return "☐ " & note.text
proc render(note: ReminderNote): string =
return note.due_timestamp.format("yyyy-MM-dd") & " " & note.text
let textNote = Note(text: "Just a plain note")
let taskNote = TaskNote(text: "Do me soon!", completed: false, parent: textNote)
let reminderNote = ReminderNote(text: "Don't forget about me...", due_timestamp: now() + 1.months, parent: taskNote)
let childNote = ReminderNote(text: "Seriously, don't forget!", due_timestamp: now() + 1.hours + 1.months, parent: reminderNote)
echo textNote.render()
echo taskNote.render()
echo reminderNote.render()
echo childNote.render()
# output:
# Just a plain note
# ☐ Do me soon!
# 2019-04-19 Don't forget about me...
# 2019-04-19 Seriously, don't forget!
So far so good. But what if we want to render a note's parent note? Which render procedure will get called?
echo childNote.parent.render() # Don't forget about me...
It jumped all the way back to the render proc for the type Note, even though its parent was a ReminderNote. This is because Nim procs use static dispatch.
Static vs Dynamic Dispatch in Nim
Static dispatch implies that Nim should know the type of an object at compile time. This allows it to sort of "lock in" which procedure will be called, which improves performance, allows for compile time evaluation, and dead code elimination. (For this reason, you should typically use procs unless you explicitly need methods.)
In this example, since our parent property is assigned as a Note type, when Nim compiles the program it locks in the Note render proc for the parent. It will only ever be rendered and handled as a Note.
Unless we have a way to do dynamic dispatch!
If we use the method keyword instead of the proc keyword on our render functions, we're telling Nim to not lock in the function call signatures at compile time. Instead it generates a dispatch tree which is evaluated at run time.
import times
type
Note = ref object of RootObj
text: string
parent: Note
TaskNote = ref object of Note
completed: bool
ReminderNote = ref object of Note
due_timestamp: DateTime
method render(note: Note): string =
return note.text
method render(note: TaskNote): string =
case note.completed
of true:
return "☑ " & note.text
of false:
return "☐ " & note.text
method render(note: ReminderNote): string =
return note.due_timestamp.format("yyyy-MM-dd") & " " & note.text
let textNote = Note(text: "Just a plain note")
let taskNote = TaskNote(text: "Do me soon!", completed: false, parent: textNote)
let reminderNote = ReminderNote(text: "Don't forget about me...", due_timestamp: now() + 1.months, parent: taskNote)
let childNote = ReminderNote(text: "Seriously, don't forget!", due_timestamp: now() + 1.hours + 1.months, parent: reminderNote)
echo textNote.render()
echo taskNote.render()
echo reminderNote.render()
echo childNote.render()
# output:
# Just a plain note
# ☐ Do me soon!
# 2019-04-19 Don't forget about me...
# 2019-04-19 Seriously, don't forget!
echo childNote.parent.render()
# 2019-04-19 Don't forget about me...
Perfect!
Nim allows for a lot of flexibility in the style of programming you want to perform, but with that comes a great deal of complexity that may not always be immediately obvious. In this case, since we were using procs instead of methods, we simply called the incorrect render function in some cases. There would be no error raised, and we might not have even noticed the issue right away. This is a great case for thorough unit testing, and for thoroughly reading the docs!
In general, method should be used over proc when, and only when, you don't know at compile time which object type from your inheritance tree the function will be handling.