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 0x7f3e4ddc96d0>)
<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.