Custom Computables & Partial Evaluations¶
InQuanto provides the flexibility to create custom computables using
primitive
objects.
In this section, we demonstrate how to build and evaluate custom computables. Consider the simple
MyComputable
class defined below:
from inquanto.computables.primitive import ComputableNode, ComputableTuple, ComputableInt
class MyComputable(ComputableNode):
def __init__(self, value):
self.value = value
def evaluate(self, evaluator):
return evaluator(self)
c = ComputableTuple(ComputableInt(2), 3, MyComputable("value"))
print(c.evaluate(evaluator=lambda v: v.value + "_evaluated"))
(2, 3, 'value_evaluated')
The evaluator here is a simple lambda function.
To facilitate the evaluation of custom computables, you may need to create custom evaluators.
These evaluators can range from simple functions to class methods utilizing decorators for
method dispatching. Furthermore, the computable type can control how the computable instance
should be evaluated. For example, below we introduce MyOtherComputable
and two
different styles of evaluators that are capable of handling both custom computables:
class MyOtherComputable(ComputableNode):
def __init__(self, value):
self.value = value
def evaluate(self, evaluator):
return evaluator(self)
c = ComputableTuple(ComputableInt(2), 3, MyComputable("value"), MyOtherComputable("other_value"))
def my_evaluator(node):
if isinstance(node, MyComputable):
return node.value + "_evaluated"
elif isinstance(node, MyOtherComputable):
return node.value + "_evaluated_differently"
print(c.evaluate(evaluator=my_evaluator))
from functools import singledispatchmethod
class MyEvaluator:
@singledispatchmethod
def __call__(self, node):
raise NotImplementedError(f"{node} does not have an evaluator")
@__call__.register(MyComputable)
def _(self, node):
return node.value + "_evaluated_in_class"
@__call__.register(MyOtherComputable)
def _(self, node):
return node.value + "_evaluated_differently_in_class"
print(c.evaluate(evaluator=MyEvaluator()))
(2, 3, 'value_evaluated', 'other_value_evaluated_differently')
(2, 3, 'value_evaluated_in_class', 'other_value_evaluated_differently_in_class')
Partial evaluation of the computable tree is also allowed, provided the evaluator is
designed as such. For example, the MyPartialEvaluator
below is only capable of
evaluating MyComputable
, but not MyOtherComputable
:
class MyPartialEvaluator:
@singledispatchmethod
def __call__(self, node):
return node
@__call__.register(MyComputable)
def _(self, node):
return node.value + "_evaluated_in_class"
print(c.evaluate(evaluator=MyPartialEvaluator()))
# But note that the return value in case of partial evaluation is still a computable
# That is
print(type(c.evaluate(evaluator=MyPartialEvaluator())))
# whereas a complete evaluation results in a bare python type
print(type(c.evaluate(evaluator=MyEvaluator())))
# Therefore, multiple evaluators can be applied in sequence, if the first one is partial evaluation
c_partial = c.evaluate(evaluator=MyPartialEvaluator())
print(c_partial.evaluate(evaluator=MyEvaluator()))
(2, 3, 'value_evaluated_in_class', <__main__.MyOtherComputable object at 0x7f3cad362410>)
<class 'inquanto.computables.primitive.collections._tuple.ComputableTuple'>
<class 'tuple'>
(2, 3, 'value_evaluated_in_class', 'other_value_evaluated_differently_in_class')
Custom evaluators and custom computables will work in harmony with other computables. For example:
from inquanto.computables.primitive import ComputableFunction
cf = ComputableFunction(lambda x, y: f"{x}_{y}", MyComputable("one"), MyOtherComputable("two"))
print(cf.evaluate(evaluator=MyEvaluator()))
one_evaluated_in_class_two_evaluated_differently_in_class
On evaluation, the evaluate()
method walks
over the ComputableFunction
expression tree and invokes
MyEvaluator
where applicable.