I have been writing this draft for several days , and got a little mixed up verbalising these ideas , which are simpler to understand using code and examples…
If you want to jump straight into code, there is a link at the end.
Function = a module wide constant symbol ( e.g :sum) without any specifications regarding types of input variables or output
Method = a function definition with upper bounds on types of input argument
MethodTable = each Function has a MethodTable that encapsulates all methods associated with that Function
MethodInstance = each Method may compile to several MethodInstance depending on the input types
Julia lets you define different methods (pieces of code) for the same function(a module wide const symbol) within a module. The method themselves may cover a range of input arguments. each set of runtime arguments gets jited on demand to an actual binary called a MethodInstance.
This in itself is not that flexible since you are limited to only the methods in the module. So Julia lets you
add methods to the method table of functions in other modules.
This fact introduces great flexibility for the programmer and is one of the characteristics of Julia.
So where is the problem?
Since importing a module may change other modules (for example if that module adds a method to another module) , caching the binary MethodInstances of a previous use of a module becomes impossible … or at least very volatile.
For small projects this is ok, code gets jitted on demand, first call to any function is slow, but things get quick
Moreover parts of Base functions , are “baked” into the sys.so , calling those functions will use the cache
instead of re-jitting. This is a kind of a “cheat” because it is legal to do only for @pure functions … however it is
necessary in order to get the REPL responsive quickly.
For large projects , the time cost of jitting all dependencies can become a show stopper for using Julia.
Oh my that does sound sub-optimal…
It is unfortunatley, but there is a way to make Julia completely cachable
yes completely , meaning that as long as you don’t change a module(define a new global name in it) all
the jitted code can be cached , so Julia will become: slow the first you use a function only if it is the first time ever!
Julia can become an effective mean to distribute cross-platform binaries , with source code attached and easily editable … I can imagine the equivalent of huge projects like openCV and tensorFlow.
And it will be more responsive the more you use it.
Dispatch according the context of the call.
If module M imports module B and each defines a function with the same name e.g
where M.addxy is meant as an extension to B.addxy. Then instead of adding M.addxy to the method table of B.addxy (which is the way it is currently) and thus invalidating B’s binary cache, do the opposite.
Any call to M.addxy will both B and M methods and will see dispatch to the most specific method.
Wait, thats not good enough, what if B.addxy calls another function in B which is also imported in M,
How does it know to consider module M?
To solve this we need to introduce a new concept: context call:
calling function f from module B with the context of module M, will resolve to the most appropriate method
visible from module B and module M if M.f imports B.f.
Another change that is needed to achieve this is passing the context down the AST, so functions g,h etc which are called from within function f will also dispatch according to the parent function context.
It must be too complicated to implement…
Since Julia is such an expressive language , I was able to put up a small POC(~100 lines of code) using standard Julia constructs without going deeper into the C-code. The code proves that multiple dispatch according to context is feasible.
the Proof Of Concept is in https://github.com/TsurHerman/MultiFunctions
And it lets you test the ideas presented here.