3 ms·
Sure, it's not too hard. You need a new kind of function--let's call it a generic function. A regular function jumps to a user-defined body when called. A ge
by mikelevins 4y ago
Sure, it's not too hard.
You need a new kind of function--let's call it a generic function.
A regular function jumps to a user-defined body when called.
A generic function is a bit more complicated: instead of jumping to a user-defined body, it jumps to a standard dispatch routine. The dispatch routine computes some discriminator (typically a sequence of types) from the passed arguments, looks up the discriminator to find a matching function body (typically user-defined), and jumps to that. You just need to implement the lookup routine and the discriminator computation and package them up in a structure that represents a generic function.
When user source code applies a generic function to some arguments, your Lisp uses the discriminator function to compute the discriminator, looks up that discriminator using the lookup routine to find the right function body, then passes the argument values to the function body.
There are some more details you can mess with, depending on what semantics you want to support. For example, do you want flat dispatch, with a one-to-one mapping from argument types to methods? Or do you want inheritance, so that you can define methods that are applicable to a type and all its subtypes?
To put it another way, do you want to be able to define "(add x y)" once for all x and y? Then you need inheritance. Or are you satisfied to explicitly write a separate method for each and every distinct combination of x and y for all possible types? If you're okay with that, then flat dispatch will do fine.
Flat dispatch is much simpler to implement. Basically, you can just compute the type of each argument and look up the sequence of types in a hash table. When you define a method:
(define add ((x SmallInteger)(y SmallInteger))
...)
you put the body of that definition in a hash table under the key `(SmallInteger SmallInteger)`. When user code calls `(add 2 3)`, your discriminator turns `(2 3)` into `(SmallInteger SmallInteger)` and Bob's your uncle. Look it up, get function body, profit!
The downside of this approach is that you have to explicitly write a definition like that for each and every separate numeric type that you want to support.
Inheritance fixes that problem; you can do this instead:
(define add ((x Number)(y Number))
...)
...and now your method body applies to every kind of argument that is a subtype of Number. The user of your Lisp thanks you.
This solution comes at considerable cost to you, the implementor, though. You can't just compute the types and look them up in a hash table anymore. If there's no entry for `(SmallInteger SmallInteger)`, you need to check for `(Integer SmallInteger)`, and `(SmallIteger Integer)`, and `(Integer Integer)`, and `(Real SmallInteger)`, and `(SmallIteger Real)`, and so on and so forth for all permutations of all possible subtypes of Number.
In addition, if you support multiple inheritance as well as multiple dispatch, then the supertypes of each type form a graph. You have to work out how to linearize that graph for each argument so that your dispatch routine always sees the same deterministic sequence of supertypes every time, so that your dispatch algorithm will be deterministic. The choice of linearization algorithm has significant consequences for how reasonable and comprehensible your dispatch results are. If you choose a bad one then your users will spend a lot of time trying to figure out why they get totally unexpected results all the time.
So flat dispatch is WAY less work to implement, though it does come at some cost in drudgery for users of your Lisp. I guess if you mean for your Lisp to be used by other people for real work, then it's worth considering support for inheritance, but if you're just working on a hobby Lisp to learn more about how implementation works, then flat dispatch is probably the way to go.
Also, it's a valid option to just decide you don't want to mess with the whole inheritance can of worms, so users will just have to live with flat dispatch. There are some relatively simple things you can do to make it a little more convenient--for example, supporting default dispatch (that is, methods that automatically apply when the arguments don't match any defined method).