-
Notifications
You must be signed in to change notification settings - Fork 28
Evaluation Design
The evaluation module has vaguely monad-like processing.
It might make sense to advance this to proper monad processing.
A Result monad might be a better design than a Result type which is a celpy.celtypes.Value | CELEvalError | celpy.celtypes.CELType.
See Chapter 13 of Functional Python Programming.
An important note is the way the "short-circuit" operators (i.e., _&&_, _||_, and _?_:_) work.
In order to permit replacing this functions, the implementation evaluates all operands and the resolves a final result from the operand values. This involves monad-like processing of BoolType | CELEvalError object. This is inefficient, but simple. An alternative is to provide function closures instead of values: the function can then compute values of only the necessary operands.
The results of CEL expressions (the Result type) needs to be a subclass of PyMonad Either.
The Right branch is a proper Value | CELType. The Left branch is a CELEvalError.
Using an Either instance would clarify processing, and reduce the need for some of the isinstance() overheads.
For example, this can simplify the CEL && and || operators.
If there are two Right branches, the operator is applied to these values.
If there's one Right branch, this value is the result.
Otherwise, there are two Left branches, and one of these is returned.
Further, we can consider pushing the operation of mapping a Python exception to a CELEvalError object out of the Evaluator class.
This could be refactored down into the methods of various celtypes type definitions.
This would require wrapping many methods with the @eval_error decorator.
The implementations would be __add__ = eval_error(class.__add__) to wrap the superclass method with a decorator.
(A metaclass could automate this.)
Introducing this monad may also reduce the need for the function_eval() and method_eval() methods of the Evaluation class.
There several interim solutions that avoid a breaking change to the way celtypes exceptions work.
Define a version of eval() called result().
This function evaluates a string, mapping Python exceptions to CELEvalError objects.
It will behave somewhat like the @eval_error decorator and the Evaluator.function_eval() method.
The result() function will be used by the Transpiler class around exposed, native Python operations.
This catches the native exception, mapping it to CELEvalError.
This is complicated by the fact that there would be nested result("some_string") functions.
Consider result("result('logical_or('result(\"true\")', 'result(\"42 / 0\")')").
The nested quotes become a bit of a problem for making sure the whole thing parses properly.
This can be worked around, by emitting a result()-based sub-expressions as separate named strings.
e_1l = "celtypes.BoolType(True)"
e_1r = "celtypes.IntType(42) / celtypes.IntType(0)"
e_1 = logical_or(result(e_1l), result(e_1r))
CEL = e_1We can define result() using the @eval_error decorator:
result = eval_error(eval)This kind of multi-step processing is needed for _||_, _&&_, and _?_:_, where there are expressions that may raise an exception, and the exception needs to be treated like the Left branch of an Either.
This may also be required for any macros that silently absorb exceptions.
This means the overall CEL expression becomes a sequence of statements.
The final statement sets any easy-to-find CEL variable to the result.
Instead of strings for expressions that need to be wrapped in a try:/except: block, create lambda objects.
This can be passed to the result() function for evaluation.
This has the tiny advantage of avoiding eval(), which many folks find scary.
Each lambda can accept an Activation instance with variable bindings.
e_1l = lambda a: celtypes.BoolType(True)
e_1r = lambda a: celtypes.IntType(42) / celtypes.IntType(0)
e_1 = logical_or(result(e_1l), result(e_1r))
CEL = e_1This relies on a global the_activation to provide the activations that will be used to compute the result.
The final statement sets any easy-to-find CEL variable to the result.
It can be wrapped in template code:
def cel_expr(act: Activation) -> Result:
global the_activation
the_activation = act
${CEL_body}
return CELThe result() function, used to map Python exceptions to CELEvalError objects, could be this:
def result(expr: Callable[[Activation], Result]) -> Result:
global the_activation
try:
return expr(the_activation)
except (ValueError, KeyError, TypeError, ZeroDivisionError, OverflowError) as ex:
return CELEvalError(str(ex), ex)Or, an alternative is this:
def cel_eval(expr: Callable[[Activation], Result]) -> Result:
global the_activation
return expr(the_activation)
result = eval_error(cel_eval)Revise the celtypes package to raise CELEvalError exceptions instead of native Python exceptions.
This is a potentially profound API change for any application using or extending the celtypes classes.
It's also much more useful, since the native Python operators -- when applied to celtypes class instances -- would properly raise CELEvalError without the need for an additional @eval_error decoration.
Consider the following docstring from celtypes.
CEL types will raise :exc:
ValueErrorfor out-of-range values and :exc:TypeErrorfor operations they refuse. The :py:mod:evaluationmodule can capture these exceptions and turn them into result values. This can permit the logic operators to quietly silence them via "short-circuiting".
In the normal course of events, CEL's evaluator may attempt operations between a CEL exception result and an instance of one of CEL types. We rely on this leading to an ordinary Python :exc:
TypeErrorto be raised to propogate the error. Or. A logic operator may discard the error object.
The :py:mod:
evaluationmodule extends these types with it's own :exc:CELEvalErrorexception. We try to keep that as a separate concern from the core operator implementations here. We leverage Python features, which means raising exceptions when there is a problem.
This needs to be altered to reflect a somewhat simpler design:
-
CELEvalErroris part ofceltypes. -
The native
ValueErrorandTypeErrorexceptions are not raise, insteadCELEvalErroris used.