Medleys
Warning
This is a new feature and there may be a few rough edges.
Medleys are a novel and comprehensive way to define and combine functionality. Classes that inherit from ovld.Medley are free-form mixins that you can (almost) arbitrarily combine together.
Example
from ovld import Medley
class Walk(Medley):
"""This medley walks through lists and dicts."""
def __call__(self, x: list):
return [self(item) for item in x]
def __call__(self, x: dict):
return {k: self(v) for k, v in x.items()}
def __call__(self, x: object):
return x
class Punctuate(Medley):
"""This medley punctuates strings."""
punctuation: str = "."
def __call__(self, x: str):
return f"{x}{self.punctuation}"
class Multiply(Medley):
"""This medley multiplies integers by a factor."""
factor: int = 2
def __call__(self, x: int):
return x * self.factor
# You can arbitrarily combine instances
walk = Walk()
assert walk([10, "hello"]) == [10, "hello"]
walkp = Walk() + Punctuate("!!!!")
assert walkp([10, "hello"]) == [10, "hello!!!!"]
walkm = Walk() + Multiply(300)
assert walkm([10, "hello"]) == [3000, "hello"]
walkpm = Walk() + Punctuate("!!!!") + Multiply(300)
assert walkpm([10, "hello"]) == [3000, "hello!!!!"]
# You can also combine classes
walkp = (Walk + Punctuate)(punctuation="!!!!")
assert walkp([10, "hello"]) == [10, "hello!!!!"]
walkm = (Walk + Multiply)(factor=300)
assert walkm([10, "hello"]) == [3000, "hello"]
walkpm = (Walk + Punctuate + Multiply)(punctuation="!!!!", factor=300)
assert walkpm([10, "hello"]) == [3000, "hello!!!!"]
Usage
All medleys are dataclasses and you must define their data fields as you would for a normal dataclass (using dataclass.field if needed). When combining medleys, fields are forced to be keyword only except for the first class in the mix.
Warning
You may not define __init__ in a Medley, because it would interfere with combining them with the + operator.
- As with standard dataclasses, define
__post_init__in order to perform additional tasks after initilization. Melded classes will run all__post_init__functions. - There can be multiple implementations of any function. All functions will be wrapped with
ovld. - Melding multiple classes together means melding all of their methods.
- If two implementations have the exact same signature, the last one will override the others.
from ovld import Medley
class Counter(Medley):
start: int = 0
def __post_init__(self):
self._current = self.start
def count(self):
self._current += 1
return self._current
class Greeter(Medley):
name: str = "John"
username: str = None
def __post_init__(self):
self.username = self.name.lower() if self.username is None else self.username
def greet(self):
return f"Hello {self.name}, your username is {self.username}"
CountGreet = Counter + Greeter
cg = CountGreet(start=10, name="Barbara")
assert cg.count() == 11
assert cg.count() == 12
assert cg.greet() == "Hello Barbara, your username is barbara"
Combining inplace
It is possible to combine medley classes inplace with extend or +=. Existing instances will gain the new behaviors and the default values of the new fields (the new fields must define default values).
# Continuation of the example up top
walk = Walk()
assert walk([10, "hello"]) == [10, "hello"]
Walk.extend(Punctuate, Multiply)
assert walk([10, "hello"]) == [30, "hello."]
Alternative combiners
Multiple dispatch with ovld is the default way multiple implementations of the same method are combined, but there are others, which you can declare like this:
from ovld.medley import KeepLast, RunAll, ReduceAll, ChainAll
class Custom(Medley):
fn1 = KeepLast() # Only the last implementation will be valid (default Python behavior)
fn2 = RunAll() # All implementations will be run (e.g. __post_init__ = RunAll())
fn3 = ReduceAll() # Combine as: impl_C(impl_B(impl_A(arg)))
fn4 = ChainAll() # Combine as: obj.impl_A(*args).impl_B(*args).impl_C(*args)
def fn1(self):
...
...
Setting a field to a Combiner only declares the combiner to use for the field with that name, it does not set the actual attribute. Only the first implementation will do that.
Code generation
Warning
Code generation is EXPERIMENTAL and the interface may break at any moment!
Medleys are compatible with ovlds that generate code. However, code generation happens at the class level, so any field which is used in the context of code generation must be annotated as CodegenParameter[type], to ensure its availability. The first argument to the generator will be a subclass of the original class, tweaked for the particular values of the codegen parameters.
Only the CodegenParameter[]-annotated fields should be accessed during codegen.
from ovld import Medley, CodegenParameter, Lambda, Code, code_generator
class CaseChanger(Medley):
upper: CodegenParameter[bool] = True
@code_generator
def __call__(cls, x: str):
method = str.upper if cls.upper else str.lower
return Lambda(Code("$method($x)", method=method))
to_upper = Walk() + CaseChanger(True)
assert to_upper(["Hello", "World"]) == ["HELLO", "WORLD"]
to_lower = Walk() + CaseChanger(False)
assert to_lower(["Hello", "World"]) == ["hello", "world"]