Package: ovld
OvldMultiple dispatch in Python, with some extra features.With ovld, you can write a version of the same function for every type signature using annotations instead of writing an awkward sequence ofisinstancestatements. Unlike Pythonsingledispatch, it works for multiple arguments.Other features ofovld:Multiple dispatch for methods (withmetaclass=ovld.OvldMC)Create variants of functionsBuilt-in support for extensible, stateful recursionFunction wrappersFunction postprocessorsNice stack tracesExampleHere's a function that adds lists, tuples and dictionaries:fromovldimportovld@ovlddefadd(x:list,y:list):return[add(a,b)fora,binzip(x,y)]@ovlddefadd(x:tuple,y:tuple):returntuple(add(a,b)fora,binzip(x,y))@ovlddefadd(x:dict,y:dict):return{k:add(v,y[k])fork,vinx.items()}@ovlddefadd(x:object,y:object):returnx+yBootstrapping and variantsNow, there is another way to do this using ovld'sauto-bootstrapping. Simply listselfas the first argument to the function, andselfwill be bound to the function itself, so you can callself(x, y)for the recursion instead ofadd(x, y):@ovlddefadd(self,x:list,y:list):return[self(a,b)fora,binzip(x,y)]@ovlddefadd(self,x:tuple,y:tuple):returntuple(self(a,b)fora,binzip(x,y))@ovlddefadd(self,x:dict,y:dict):return{k:self(v,y[k])fork,vinx.items()}@ovlddefadd(self,x:object,y:object):returnx+yWhy is this useful, though? Observe:@add.variantdefmul(self,x:object,y:object):returnx*yassertadd([1,2],[3,4])==[4,6]assertmul([1,2],[3,4])==[3,8]Avariantof a function is a copy which inherits all of the original's implementations but may define new ones. And becauseselfis bound to the function that's called at the top level, the implementations forlist,tupleanddictwill bindselftoaddormuldepending on which one was called. You may also callself.super(*args)to invoke the parent implementation for that type.StateYou can passinitial_stateto@ovldorvariant. The initial state must be a function that takes no arguments. Its return value will be available inself.state. The state is initialized at the top level call, but recursive calls toselfwill preserve it.In other words, you can do something like this:@add.variant(initial_state=lambda:0)defcount(self,x,y):self.state+=1return(f"#{self.state}",x+y)assertcount([1,2,3],[4,5,6])==[("#1",5),("#2",7),("#3",9)]The initial_state function can return any object and you can use the state to any purpose (e.g. cache or memoization).Custom dispatchYou can define your own dispatching function. The dispatcher's first argument is alwaysself.self.resolve(x, y)to get the right function for the types of x and yself[type(x), type(y)]will also return the right function for these types, but it works directly with the types.For example, here is how you might define a function such that f(x) <=> f(x, x):@ovld.dispatchdefadd_default(self,x,y=None):ifyisNone:y=xreturnself.resolve(x,y)(x,y)@ovlddefadd_default(x:int,y:int):returnx+y@ovlddefadd_default(x:str,y:str):returnx+y@ovlddefadd_default(xs:list,ys:list):return[add_default(x,y)forx,yinzip(xs,ys)]assertadd_default([1,2,"alouette"])==[2,4,"alouettealouette"]There are other uses for this feature, e.g. memoization.The normal functions may also have aself, which works the same as bootstrapping, and you can give aninitial_stateto@ovld.dispatchas well.Postprocess@ovld,@ovld.dispatch, etc. take apostprocessargument which should be a function of one argument. That function will be called with the result of the call and must return the final result of the call.Note that intermediate, bootstrapped recursive calls (recursive calls usingself()) willnotbe postprocessed (if you want to wrap these calls, you can do so otherwise, like defining a custom dispatch). Only the result of the top level call is postprocessed.MethodsUse theOvldMCmetaclass to use multiple dispatch on methods. In this case there is no bootstrapping as described above andselfis simply bound to the class instance.fromovldimportOvldMCclassCat(metaclass=OvldMC):definteract(self,x:Mouse):return"catch"definteract(self,x:Food):return"devour"definteract(self,x:PricelessVase):return"destroy"Subclasses ofCatwill inherit the overloadedinteractand it may define additional overloaded methods which will only be valid for the subclass.Note:It is possible to useovld.dispatchon methods, but in this case be aware that the first argument for the dispatch method will not be the usualselfbut anOvldCallobject. Theselfcan be retrived asovldcall.obj. Here's an example to make it all clear:classStuff(metaclass=OvldMC):def__init__(self,mul):self.mul=mul@ovld.dispatchdefcalc(ovldcall,x):# Wraps every call to self.calc, but we receive ovldcall instead of self# ovldcall[type(x)] returns the right method to call# ovldcall.obj is the self (the actual instance of Stuff)returnovldcall[type(x)](x)*ovldcall.obj.muldefcalc(self,x:int):returnx+1defcalc(self,xs:list):return[self.calc(x)forxinxs]print(Stuff(2).calc([1,2,3]))# [4, 6, 8, 4, 6, 8]Mixins in subclassesThe@extend_superdecorator on a method will combine the method with the definition on the superclass:fromovldimportOvldMC,extend_superclassOne(metaclass=OvldMC):deff(self,x:int):return"an integer"classTwo(One):@extend_superdeff(self,x:str):return"a string"assertTwo().f(1)=="an integer"assertTwo().f("s")=="a string"Ambiguous callsThe following definitions will cause a TypeError at runtime when called with two ints, because it is unclear which function is the right match:@ovlddefambig(x:int,y:object):print("io")@ovlddefambig(x:object,y:int):print("oi")ambig(8,8)# ???You may define an additional function with signature (int, int) to disambiguate:@ovlddefambig(x:int,y:int):print("ii")Other featuresmetaTo test arbitrary conditions, you can usemeta:fromovldimportovld,meta@metadefStartsWithT(cls):returncls.__name__.startswith("T")@ovlddeff(x:StartsWithT):return"T"assertf(TypeError("xyz"))=="T"# Or: a useful example, since dataclasses have no common superclass:fromdataclassesimportdataclass,is_dataclass@dataclassclassPoint:x:inty:int@ovlddeff(x:meta(is_dataclass)):return"dataclass"assertf(Point(1,2))=="dataclass"deferredYou may define overloads for certain classes from external packages without
having to import them:fromovldimportovld,deferred@ovlddeff(x:deferred("numpy.ndarray")):return"ndarray"# numpy is not importedassert"numpy"notinsys.modules# But once we import it, the ovld works:importnumpyassertf(numpy.arange(10))=="ndarray"Tracebacksovldautomagically renames functions so that the stack trace is more informative:@add.variantdefbad(self,x:object,y:object):raiseException("Bad.")bad([1],[2])"""File "/Users/breuleuo/code/ovld/ovld/core.py", line 148, in bad.entryres = ovc(*args, **kwargs)File "/Users/breuleuo/code/ovld/ovld/core.py", line 182, in bad.dispatchreturn method(self.bind_to, *args, **kwargs)File "example.py", line 6, in bad[list, list]return [self(a, b) for a, b in zip(x, y)]File "example.py", line 6, in <listcomp>return [self(a, b) for a, b in zip(x, y)]File "/Users/breuleuo/code/ovld/ovld/core.py", line 182, in bad.dispatchreturn method(self.bind_to, *args, **kwargs)File "example.py", line 26, in bad[*, *]raise Exception("Bad.")Exception: Bad."""The functions on the stack have names likebad.entry,bad.dispatch,bad[list, list]andbad[*, *](*stands forobject), which lets you better understand what happened just from the stack trace.This also means profilers will be able to differentiate between these paths and between variants, even if they share code paths.