Okay guys, this is something that blows my mind away! I've wiped everything redundant from my code and added Star Wars to make it funnier, but the problem is really terrifying me.
Consider we have a class Hero and a method for a Hero to kill another Hero:
# heroes.nim
type
Hero* = object of RootObj
method kill*(self: var ref Hero, victim: ref Hero) {.base, inline.} = discard
# by default no one can kill another Hero. kill() just does nothing
Now we create a bit of Jedi:
# jedi.nim
import heroes
type
OB1* = object of Hero
Yoda* = object of Hero
method kill*(self: var ref Hero, victim: ref OB1) = echo("Obi Wan Kenobi was killed")
method kill*(self: var ref Hero, victim: ref Yoda) = echo("Yoda was killed")
# Even Jedi may be killed!
For the sith, let's add a Startrooper. I never liked them so we won't use it anywhere. However, its role is significant! (see below)
# sith.nim
import heroes
type
Startrooper* = object of Hero
method kill*(self: var ref Hero, victim: ref Startrooper) = echo("Startrooper was killed... one more")
Now let's play!
# main.nim
import heroes
import jedi
# import sith # <-- leave for now
# this method will protect Yoda from being killed by anyone
method kill(self: var ref Hero, victim: ref Yoda) = echo("Yoda may not be killed!")
var
darthVader = new(ref Hero)
yoda = new(ref Yoda)
# this call matches exactly the newly defined method above
darthVader.kill(yoda) # outputs "Yoda may not be killed!", as expected
Now we add sith...
# main.nim
import heroes
import jedi
import sith # <-- uncomment this
# this method will protect Yoda from being killed by anyone
method kill(self: var ref Hero, victim: ref Yoda) = echo("Yoda may not be killed!")
var
darthVader = new(ref Hero)
yoda = new(ref Yoda)
# this call matches exactly the newly defined method above
darthVader.kill(yoda) # outputs "Yoda was killed"... WAT?!
Do you see it too? We added a method to kill a Startrooper (method kill(self: var ref Hero, victim: ref Startrooper)) and suddenly our Yoda's protection is broken! What the hell?!
Now leave sith module included and let's comment killing Obi Wan Kenobi:
# jedi.nim
import heroes
type
OB1* = object of Hero
Yoda* = object of Hero
# method kill*(self: var ref Hero, victim: ref OB1) = echo("Obi Wan Kenobi was killed")
method kill*(self: var ref Hero, victim: ref Yoda) = echo("Yoda was killed")
Guess what?! "Yoda may not be killed!"
So, when you add sith module with Startrooper killing, Yoda's protection becomes broken, but when you make Kenobi immortal, Yoda's protection is back!
Now here's the question: how close to schizophrenia I am?
I think this is due to undefined behavior. I couldn't find anywhere some documentation about what happens if two methods with exactly the same signature exist(there are two kill methods with the signature self: var ref Hero, victim: ref Yoda).
So in the end the compiler just accepts that two methods and called is the one which happens to be the one which comes first in the selection in the generated C code.
EDIT: BTW unless you want to reassign the reference handle itself, you don't need the var ref, ref is enough if you only want to modify the memory the reference is pointing to.
Thank you!
Yeah you got the idea! Compiler doesn't allow to redefine a proc/method in the same module, however redefinition across modules compiles fine. I wanted to use this trick to allow users redefine my library methods in their code - just like that kill in main.nim. It is much simpler than using inheritance or other methods to inject user-defined code - just use internal dispatch trees that will do all redefinition job for you :)
Undefined behaviour here is really undefined. What made me curious is that definition order is always the same (kill from main.nim is always defined last), but dispatch depends on other non-related methods which is counter-intuitive...
P.S. Thanks for var notice. Actually it is a part of a larger codebase where the reassignment really happens. I just omitted some unnecessary code.
In my (somehow lack) experience of OOP, that's not how it should work. What it means to override method is to inherit the parent type and implement its own method.
If you want to work that way (in your example), maybe closest thing is a closure, which users define their own proc/function on how the method behaves.
@mashingan Yeah, that's true. However, the proposed idea would be a very comfortable way to override library's methods. But because of UB now user has to pass either subclass with custom methods or pointers to procs to the library which I find a bit messy.
Imagine a math library which does some calculations. Inside it there's a method to calculate square root. One day user understands that he knows a better and faster square root implementation. How can he "inject" it there? Currently there's no way unless lib authors allow him (in advance) to use some setting like sqrt = ptr proc(x: float): float
P.S. I came from Python so static programming languages are big pain for me :)
Afaik, OOP (for example in Java) still need which method can be override, which final, which abstract method(/class).
If the author didn't set those info, nothing for lib's users able to override or do anything the original lib.
Actually you're incorrect that a proc can be redefined across module because each proc defined with different module has symbol name as module_name.proc_name, so if there's two procs that has same name, arity and parameters types, compiler will report the error unless you call it full the proc symbol.
This is how you do it with closure
#heroes.nim
type
Hero* = ref object of RootObj
ImplementationError* = object of Exception
method kill*(self, victim: Hero, howItKilled: proc(casualty: Hero) = nil) =
if howItKilled.isNil:
raise newException(ImplementationError,
"Hero descendants should describe how they're killed")
else:
howItKilled victim
#jedi.nim
import heroes
type
OB1* = ref object of Hero
Yoda* = ref object of Hero
method kill*(self: Hero, ob1: OB1,
howItKilled = (proc(victim: OB1) =
echo "Obi Wan Kenobi was Killed")) =
howItKilled ob1
method kill*(self: Hero, yoda: Yoda,
howItKilled = (proc(victim: Yoda) =
echo "Obi Wan Kenobi was Killed")) =
howItKilled yoda
#main.nim
import heroes
import jedi
type
Startrooper = ref object of Hero
# startrooper doesn't have implementation of how it killed
let
darthVader = new Hero
yoda = new Yoda
troop = new Startrooper
try:
darthVader.kill(cast[Hero](yoda))
except ImplementationError:
stderr.write("Error!: ", getCurrentExceptionMsg()
try:
darthVader.kill troop
except ImplementationError:
stderr.write("Error!: ", getCurrentExceptionMsg())
darthVader.kill yoda # call with default defined closure
darthVader.kill(yoda, howItKilled=(proc(yoda: Yoda) = echo "Yoda may not be killed"))