In the previous article when I explored OO mechanisms in Nim I felt I dropped the ball a bit in my Fruit example. This is a followup.
In that article we first implemented some Fruit “classes” mixing methods and procs. Then I presented a cleaned up version using methods only, and a teeny template in order to reuse a base method. This template was needed since Nim currently doesn’t have a “call-next-method-matching” for multimethods like Dylan or CLOS have. This is being discussed and I think all agree that there needs to be some mechanism so that you can call a “next lesser match” of all matching multimethods.
But I also wrote that the example can be written perfectly well using generics and procs only, thus ensuring static binding and maximum speed. But the “super call” problem also existed for procs, and the template hack was just a hack. After more experimentation I now think I found the proper Nim way to do this so let’s take a look…
Generics
In Nim we can use generics to “generate” multiple “instances” of procs, for all different combinations of static types being used to call this proc. To be precise, the compiler doesn’t necessarily need to generate multiple copies, it can deal with it using a simple case switch on types, or whatever - but its a reasonable mental model of what happens. Another mental model is that a generic proc, as long as your shit compiles, will be run with the types of the arguments at the given callsite. :)
So if we have a proc declared as proc calcPrice(self): Dollar
with no type specified for self
then Nim will consider that to be the same as proc calcPrice[T](self: T): Dollar
and collect all callsites and for each unique static type of self
being passed in, it will generate a matching calcPrice
proc. Presumably writing all these procs manually would result in equivalent code.
This is how my final fairly nice generic variant of the Fruit library looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
|
Okidoki. Before I came up with the above code which looks quite simple - I experimented with a composition style using delegation instead of inheritance, and used generics too. It did work, but it got quite messy. The above solution on the other hand feels like a simple pattern one can use to solve this “super call” issue. Now… what was the problem again? :)
Problem: When you only use procs and overload them to give implementations of calcPrice()
for Banana, Pumpkin and BigPumpkin - and also a base default implementation in the base class Fruit - then you can’t call the one in Fruit from any of the others. In other words, you can’t make a “super call”, just try it and watch the infinite recursion!
We could in theory use the type Fruit for self in the base implementation (and not a generic self
), and use a type conversion of self like Fruit(self).calcPrice()
to be able to call it - but then you will be running the calcPrice()
in Fruit with self as being a Fruit! And that’s no good, because when it subsequently calls self.reduction()
it will not resolve to the proper proc, it will only resolve to the base implementation in Fruit. So forget using abstract types as types for the self argument in base implementations of procs, it is a BAD idea.
Solution: We can factor out the base implementation of calcPrice()
under another name, like basePrice()
. Then we can easily call it from the subclasses. And the default implementation of calcPrice()
in Fruit will just call basePrice()
. And this is important: we use generic self
for the proc arguments in the Fruit class, so that we don’t lose type information when we call those procs.
And here is the test code to see it works as it should:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Conclusion
How to use generics in Nim for someone like me who haven’t used generics in a loooong time, is a bit of a head twister. But after a few hours of messing, it starts to click.
The only “issue” above I guess - is the fact that the default calcPrice()
makes an extra call to basePrice()
(but only the default, so only for Bananas) so a few cycles lost there unless Nim optimizes it away. Something that a template probably could solve of course. :)