Let's Explore Object Oriented Programming with Nim
Recently I was writing a small utility library with Nim in which I needed to deal with different object types in different ways within the same procedure. I am new to Nim, and to statically typed languages, so it's important to warn you that this is almost certainly not the correct, best, or the idiomatic way to handle this situation. If you're writing production code please look elsewhere. On the other hand, this worked for me in my situation and I thought it was a good solution.
Creating Objects in Nim
Nim is not a heavily object-oriented language by any means, but it has some minimal support built in where you can fake it if needed. An object can be defined in Nim with the type statement.
type Node = object value: string active: bool # This can then be instantiated. var knot = Node(value: "slip") # You can print the attributes of an object. echo knot # > (value: "slip", active: false
An object in Nim is more like a simple struct than a real object - it's a structured grouping of variables, but doesn't have its own methods that act on it, and there are no built in constructors or destructors. We can still create procedures that accept an object and modify it, so long as we define the input object as a var.
proc activate(node: var Node) = node.active = true activate(knot) echo knot # > (value: "slip", active: true)
Since we want this to look like we're working with an object, Nim gives us a little syntactic sugar. This does the same thing as the above code.
# Same as calling activate(knot) knot.activate() echo knot # > (value: "slip", active: true)
Now our object is beginning to look like an object!
The first parameter passed to a proc can be used to call the proc. Any additional parameters can also be passed in the method call just like you normally would.
Object Inheritance
Nim objects do have single inheritance. The base object you inherit from must always itself inherit from the built-in RootObj. These objects can each define their own properties, and they of course inherit the properties of their ancestor.
The Nim docs say you should always use reference objects when using inheritance, and in practice I've round this to be true. Instead of saying ObjectType = object of RootObj we use ObjectType = ref object of RootObj. Nim references point to a location in memory rather than directly holding the object. Because of this, if we want to print the object we have to dereference it by using the [] operator. (If you forget to do this you'll get a confusing type mismatch error that doesn't give much detail about the real problem.)
type Node = ref object of RootObj value: string active: bool type NumNode = ref object of Node type BinOp = ref object of Node left, op, right: string var numnum = NumNode(value: "3", active: true) echo numnum[] # > (value: "3", active: true) var add = BinOp( left: "7", op: "+", right: "9", active: true ) echo add[] # > (left: "7", op: "+", right: "9", value: "", active: true))
The great thing about inheritance in Nim is that we can treat an object as its parent type fairly easily. This means you can operate on a NumNode as if it's a NumNode or a Node. Procs defined for the ancestor type can be used for its children type, furthering the object-oriented cause.
Below we check the type of an object using the of syntax.
assert(numnum of NumNode) # passes assert(numnum of Node) # passes proc deactivate(node: var Node) = node.active = false # This would fail with a type mismatch if we tried it. Although if we weren't using # reference objects it would actually work. # numnum.deactivate() # With reference objects, we must treat it as a Node before using it as one Node(numnum).deactivate() echo numnum[] # > (value: "3", active: false)
What about the other way? Can we pass a Node object to a method looking for a BinOp type?
proc reverseOp(node: var BinOp) = let l = node.left node.left = node.right node.right = l echo add[] # > (left: "7", op: "+", right: "9", value: "", active: true)) add.reverseOp() echo add[] # > (left: "9", op: "+", right: "7", value: "", active: true)) let knot = Node() knot.reverseOp() # > testtypes.nim(86, 5) Error: type mismatch: got <node> # > but expected one of: # > proc reverseOp(node: var BinOp)</node>
Of course that doesn't make sense, and it fails as we would expect it to. We also cannot easily convert an ancestor object to its descendant type. In this case, BinOp(knot) would fail as an invalid object conversion.
Fooling the System
This is where things perhaps go off the rails. I'm not sure if Nim is meant to be used this way, it seems to betray the spirit of static typing. But it was also the easiest solution to my problem at the time, and it does work.
In my case, a separate module was reading the user input, parsing, and returning a tree of nodes depending on what they entered. The BinOp node would not store just strings for its left/right values, but it could have any Node type (in this simplified version, a NumNode or another BinOp). This allows for the tree structure to be defined, but we don't know what type of node it will hold be until run time.
Since objects in Nim are typed according to their ancestor, we can define the left/right properties to be of type Node. We are using reference objects, so this will allow us to store either a NumNode or a BinOp there even though we've called it a Node type variable
It looks something like this.
type Node = ref object of RootObj value: string active: bool type NumNode = ref object of Node type BinOp = ref object of Node left, right: Node op: string var add = BinOp( left: NumNode(value: "7"), op: "+", right: NumNode(value: "9"), active: true ) echo add[] # > (left: ...right: ...op: "+", value: "", active: true) var multiply = BinOp( left: add, op: "*", right: NumNode(value: "3"), active: true ) echo multiply[] # > (left: ...right: ...op: "*", value: "", active: true)
Great! Now I can hold a complex tree full of nodes of varying types. Later when I need to react accordingly I simply check the type.
import strutils # provides parseInt for us proc interpret(node: Node): int = if node of NumNode: return parseInt(node.value) if node of BinOp: let n = BinOp(node) if n.op == "+": return interpret(n.left) + interpret(n.right) if n.op == "*": return interpret(n.left) * interpret(n.right) echo interpret(multiply) # > 48
Here we check if the Node is a NumNode or a BinOp. If a NumNode we return the value. If BinOp we evaluate it based on its left/right/op, recursively calling the interpret proc until we get the final value. In this case the value is 48; our tree basically boiled down to (7 + 9) * 3. That's not the point of course! The idea is that we can pass an object around as its inherited type, and then later treat it as its actual, descendant type.
(Again, there certainly may be different and better ways to tackle the problem.)
Note that we had to manipulate the Node object back into a BinOp before operating on it, because the compiler doesn't expect a Node object to have the property op. Nim considers this a type conversion, not a type cast.