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…
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
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. :)