Predicates and Symbolic Functions#

EQL is highly extensible. You can define your own logic and integrate it into queries using Predicates for boolean checks and Symbolic Functions for transforming data.

Predicates#

A Predicate is a special class that represents a boolean condition. When you call it with symbolic variables, it doesn’t execute immediately; instead, it returns an InstantiatedVariable that becomes part of the query’s execution graph.

The HasType Predicate#

One of the most useful built-in predicates is HasType, which checks if a variable is an instance of a specific class.

from krrood.entity_query_language.predicate import HasType

# Filter 'v' to only include objects that are instances of 'Handle'
query = entity(v).where(HasType(v, ExampleHandle))

Hint

variable(Type, domain=...) already includes an implicit HasType check. Use the predicate explicitly when you need to check the type of a Attribute for example.

Symbolic Functions#

A Symbolic Function is a regular Python function decorated with @symbolic_function. When called with symbolic arguments, it defers execution until the query is evaluated.

from krrood.entity_query_language.predicate import symbolic_function

@symbolic_function
def is_even(n: int) -> bool:
    return n % 2 == 0

# Use it in a query
query = entity(r).where(is_even(r.battery))

Note

EQL provides a built-in length() symbolic function for checking the size of collections.

Warning

Symbolic attribute access covers regular attributes only. Dunder names (e.g. variable.__name__) are not resolved symbolically — they are reserved for Python’s own protocols, so intercepting them would break copy, pickling, and debugging. To read a dunder-named member of a matched object inside a query, wrap the access in a @symbolic_function:

@symbolic_function
def class_name(cls: type) -> str:
    return cls.__name__

query = entity(v).where(class_name(v).startswith("C"))

Full Example: Custom Logic#

Let’s define a custom predicate and a symbolic function to find robots with specific capabilities.

from dataclasses import dataclass
from krrood.entity_query_language.factories import variable, entity, an, Symbol
from krrood.entity_query_language.predicate import symbolic_function, Predicate

@dataclass
class ExampleRobot(Symbol):
    name: str
    load: float

@symbolic_function
def calculate_stress(load: float) -> float:
    return load * 1.5

@dataclass(eq=False)
class ExampleIsOverloaded(Predicate):
    robot: ExampleRobot
    limit: float = 10.0

    def __call__(self) -> bool:
        # This is where the actual logic happens during evaluation
        return calculate_stress(self.robot.load) > self.limit

# Data
robots = [ExampleRobot("Heavy", 8.0), ExampleRobot("Light", 2.0)]
r = variable(ExampleRobot, domain=robots)

# Query using custom logic
query = an(entity(r).where(ExampleIsOverloaded(r)))

for robot in query.evaluate():
    print(f"Overloaded Robot: {robot.name} (Load: {robot.load})")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[1], line 30
     26 
     27 # Query using custom logic
     28 query = an(entity(r).where(ExampleIsOverloaded(r)))
     29 
---> 30 for robot in query.evaluate():
     31     print(f"Overloaded Robot: {robot.name} (Load: {robot.load})")

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/backends.py:144, in SelectiveBackend.evaluate(self, expression)
    142     raise SelectiveBackendCannotResolveEllipsisMatch(expression)
    143 self._warn_or_raise_on_unresolved_cause_(expression)
--> 144 yield from self._evaluate(expression)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/backends.py:200, in EntityQueryLanguageBackend._evaluate(self, expression)
    199 def _evaluate(self, expression: Evaluable) -> Iterable:
--> 200     yield from expression._evaluate_natively_()

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/backends.py:144, in SelectiveBackend.evaluate(self, expression)
    142     raise SelectiveBackendCannotResolveEllipsisMatch(expression)
    143 self._warn_or_raise_on_unresolved_cause_(expression)
--> 144 yield from self._evaluate(expression)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/backends.py:200, in EntityQueryLanguageBackend._evaluate(self, expression)
    199 def _evaluate(self, expression: Evaluable) -> Iterable:
--> 200     yield from expression._evaluate_natively_()

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:272, in SymbolicExpression.evaluate(self, backend)
    270 SymbolGraph().remove_dead_instances()
    271 results = (self._process_result_(res) for res in self._true_results_())
--> 272 yield from itertools.islice(results, self._limit_)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:271, in <genexpr>(.0)
    262 """
    263 Evaluate the query and map the results to the correct output data structure.
    264 This is the exposed evaluation method for users.
   (...)    268     argument.
    269 """
    270 SymbolGraph().remove_dead_instances()
--> 271 results = (self._process_result_(res) for res in self._true_results_())
    272 yield from itertools.islice(results, self._limit_)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:284, in <genexpr>(.0)
    274 def _true_results_(self) -> Iterator[OperationResult]:
    275     """
    276     :return: The raw ``OperationResult`` instances from ``_evaluate_()`` whose truth
    277         value is true, before :meth:`_process_result_` maps them to output values.
   (...)    280         unfiltered regardless of that value's truthiness.
    281     """
    282     return (
    283         result
--> 284         for result in self._evaluate_()
    285         if not isinstance(self, TruthValuedExpression) or result.is_true
    286     )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:485, in SymbolicExpression._evaluate_(self, sources)
    483     yield result
    484 else:
--> 485     for result in map(
    486         self._evaluate_conclusions_and_update_bindings_,
    487         self._evaluate__(sources),
    488     ):
    489         evaluation_context.on_result_yielded(expression=self, result=result)
    490         yield result

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/query.py:650, in Query._evaluate__(self, sources)
    646 evaluation_context = get_evaluation_context()
    647 if evaluation_context is None or not self._is_nested_subquery_(
    648     evaluation_context
    649 ):
--> 650     yield from self._produce_results_(sources)
    651     return
    653 cached_stream = evaluation_context.subquery_result_cache.get_or_create(
    654     self._id_,
    655     lambda: CachedResultStream(self._produce_results_(OperationResult({}))),
    656 )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/query.py:683, in Query._produce_results_(self, sources)
    681 for transformer in self._result_transformers_:
    682     results = transformer.transform(results)
--> 683 yield from results
    685 if self._seen_results is not None:
    686     self._seen_results.clear()

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/result_transformers.py:114, in Quantification.transform(self, results)
    110 def transform(
    111     self, results: Iterator[OperationResult]
    112 ) -> Iterator[OperationResult]:
    113     if self.constraint is None:
--> 114         yield from results
    115         return
    116     number_of_results = 0

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/query.py:677, in <genexpr>(.0)
    668 def _produce_results_(self, sources: OperationResult) -> Iterator[OperationResult]:
    669     """
    670     Produce this product's results: the projected, result-mapped rows of its cartesian product.
    671 
    672     :param sources: The current bindings.
    673     :return: An iterator over the query's result rows.
    674     """
    675     results = (
    676         self._get_operation_result_(result)
--> 677         for result in self._apply_results_mapping_(
    678             self._evaluate_product_(sources),
    679         )
    680     )
    681     for transformer in self._result_transformers_:
    682         results = transformer.transform(results)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/utils.py:156, in cartesian_product_while_passing_the_bindings_around(expressions, sources)
    150     return stage
    152 expression_evaluation_generators = [
    153     _make_stage(expression) for expression in expressions
    154 ]
--> 156 yield from chain_stages(expression_evaluation_generators, sources)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/utils.py:196, in chain_stages(stages, initial)
    193     for next_result in stages[stage_index](current_result):
    194         yield from evaluate_next_stage_or_yield(stage_index + 1, next_result)
--> 196 yield from evaluate_next_stage_or_yield(0, initial)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/utils.py:193, in chain_stages.<locals>.evaluate_next_stage_or_yield(stage_index, current_result)
    191     yield current_result
    192     return
--> 193 for next_result in stages[stage_index](current_result):
    194     yield from evaluate_next_stage_or_yield(stage_index + 1, next_result)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/utils.py:145, in cartesian_product_while_passing_the_bindings_around.<locals>._make_stage.<locals>.stage(prev)
    140 def stage(prev: Optional[OperationResult]) -> Iterator[OperationResult]:
    141     """
    142     Evaluate the inner expression and combine its bindings with the previous
    143     stage's bindings.
    144     """
--> 145     for result in inner_expression._evaluate_(prev):
    146         if prev is not None:
    147             result.update(prev)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:485, in SymbolicExpression._evaluate_(self, sources)
    483     yield result
    484 else:
--> 485     for result in map(
    486         self._evaluate_conclusions_and_update_bindings_,
    487         self._evaluate__(sources),
    488     ):
    489         evaluation_context.on_result_yielded(expression=self, result=result)
    490         yield result

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/operations.py:71, in Where._evaluate__(self, sources)
     70 def _evaluate__(self, sources: OperationResult) -> Iterator[OperationResult]:
---> 71     yield from (
     72         result
     73         for result in self._evaluate_child_as_condition_(self._child_, sources)
     74         if result.is_true
     75     )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/operations.py:73, in <genexpr>(.0)
     70 def _evaluate__(self, sources: OperationResult) -> Iterator[OperationResult]:
     71     yield from (
     72         result
---> 73         for result in self._evaluate_child_as_condition_(self._child_, sources)
     74         if result.is_true
     75     )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:1123, in TruthValueOperator._evaluate_child_as_condition_(self, child, sources)
   1121 if evaluation_context is not None:
   1122     evaluation_context.truth_value_operator_children.record(child._id_)
-> 1123 for result in child._evaluate_(sources):
   1124     if result.has_value:
   1125         yield result._as_fresh_observation_()

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:485, in SymbolicExpression._evaluate_(self, sources)
    483     yield result
    484 else:
--> 485     for result in map(
    486         self._evaluate_conclusions_and_update_bindings_,
    487         self._evaluate__(sources),
    488     ):
    489         evaluation_context.on_result_yielded(expression=self, result=result)
    490         yield result

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/variable.py:261, in InstantiatedVariable._evaluate__(self, sources)
    257 def _evaluate__(
    258     self,
    259     sources: OperationResult,
    260 ) -> Iterable[OperationResult]:
--> 261     yield from self._instantiate_using_child_vars_and_yield_results_(sources)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/variable.py:287, in InstantiatedVariable._instantiate_using_child_vars_and_yield_results_(self, sources)
    277 # A callable class (Predicate / SymbolicFunction) implements HasBoundValue -- it binds the
    278 # constructed instance, or, for a value operation, its constructed-and-called value -- the
    279 # class-form counterpart of a @symbolic_function being called. A plain function/type does
    280 # not, so it binds the direct call result.
    281 bind = (
    282     self._type_._bound_value_
    283     if inspect.isclass(self._type_)
    284     and issubclass(self._type_, HasBoundValue)
    285     else self._type_
    286 )
--> 287 instance = bind(**kwargs)
    289 bindings = {self._id_: instance} | child_result.bindings
    290 result = self._build_operation_result_(bindings, child_result)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/predicate.py:266, in SymbolicCallable._bound_value_(cls, **kwargs)
    257 @classmethod
    258 def _bound_value_(cls, **kwargs) -> Any:
    259     """:return: the value this operation contributes to a query result when its arguments have
    260     concrete values -- the constructed instance itself by default (a :class:`Predicate`'s truth is
    261     then read from that instance). A value operation overrides this to its COMPUTED value.
   (...)    264         the query binds ``function(**values)``; for a callable class it binds this.
    265     """
--> 266     return cls._construct_normally_(**kwargs)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/predicate.py:253, in SymbolicCallable._construct_normally_(cls, **kwargs)
    235 @classmethod
    236 def _construct_normally_(cls, **kwargs) -> SymbolicCallable:
    237     """
    238     Construct a concrete instance directly, bypassing the symbolic ``__new__``
    239     redirect.
   (...)    251     contain.
    252     """
--> 253     instance = object.__new__(cls)
    254     instance.__init__(**kwargs)
    255     return instance

TypeError: Can't instantiate abstract class ExampleIsOverloaded without an implementation for abstract method '_verbalization_fragment_'

API Reference#