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:81, in SelectiveBackend.evaluate(self, expression)
     79 if isinstance(expression, Match) and expression.has_ellipsis_attributes:
     80     raise SelectiveBackendCannotResolveEllipsisMatch(expression)
---> 81 yield from self._evaluate(expression)

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

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/backends.py:81, in SelectiveBackend.evaluate(self, expression)
     79 if isinstance(expression, Match) and expression.has_ellipsis_attributes:
     80     raise SelectiveBackendCannotResolveEllipsisMatch(expression)
---> 81 yield from self._evaluate(expression)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/backends.py:137, in EntityQueryLanguageBackend._evaluate(self, expression)
    136 def _evaluate(self, expression: Evaluable) -> Iterable:
--> 137     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:226, in SymbolicExpression.evaluate(self, backend)
    222 SymbolGraph().remove_dead_instances()
    223 results = (
    224     self._process_result_(res) for res in self._evaluate_() if res.is_true
    225 )
--> 226 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:224, in <genexpr>(.0)
    214 """
    215 Evaluate the query and map the results to the correct output data structure.
    216 This is the exposed evaluation method for users.
   (...)    220     argument.
    221 """
    222 SymbolGraph().remove_dead_instances()
    223 results = (
--> 224     self._process_result_(res) for res in self._evaluate_() if res.is_true
    225 )
    226 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:398, in SymbolicExpression._evaluate_(self, sources)
    396     yield result
    397 else:
--> 398     for result in map(
    399         self._evaluate_conclusions_and_update_bindings_,
    400         self._evaluate__(sources),
    401     ):
    402         evaluation_context.on_result_yielded(expression=self, result=result)
    403         yield result

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/query.py:548, in Query._evaluate__(self, sources)
    544 evaluation_context = get_evaluation_context()
    545 if evaluation_context is None or not self._is_nested_subquery_(
    546     evaluation_context
    547 ):
--> 548     yield from self._produce_results_(sources)
    549     return
    551 cached_stream = evaluation_context.subquery_result_cache.get_or_create(
    552     self._id_,
    553     lambda: CachedResultStream(self._produce_results_(OperationResult({}))),
    554 )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/query.py:581, in Query._produce_results_(self, sources)
    579 for transformer in self._result_transformers_:
    580     results = transformer.transform(results)
--> 581 yield from results
    583 if self._seen_results is not None:
    584     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:575, in <genexpr>(.0)
    566 def _produce_results_(self, sources: OperationResult) -> Iterator[OperationResult]:
    567     """
    568     Produce this product's results: the projected, result-mapped rows of its cartesian product.
    569 
    570     :param sources: The current bindings.
    571     :return: An iterator over the query's result rows.
    572     """
    573     results = (
    574         self._get_operation_result_(result)
--> 575         for result in self._apply_results_mapping_(
    576             self._evaluate_product_(sources),
    577         )
    578     )
    579     for transformer in self._result_transformers_:
    580         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:398, in SymbolicExpression._evaluate_(self, sources)
    396     yield result
    397 else:
--> 398     for result in map(
    399         self._evaluate_conclusions_and_update_bindings_,
    400         self._evaluate__(sources),
    401     ):
    402         evaluation_context.on_result_yielded(expression=self, result=result)
    403         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:850, in TruthValueOperator._evaluate_child_as_condition_(self, child, sources)
    835 def _evaluate_child_as_condition_(
    836     self, child: SymbolicExpression, sources: Optional[OperationResult]
    837 ) -> Iterator[OperationResult]:
    838     """
    839     Evaluate ``child`` and apply truth-value semantics to each result.
    840 
   (...)    848     :return: An iterator of OperationResult instances with correct truth values.
    849     """
--> 850     for result in child._evaluate_(sources):
    851         if result.has_value:
    852             yield OperationResult(
    853                 result.bindings,
    854                 result.is_condition_false,
    855                 result.operand,
    856                 result.previous_operation_result,
    857             )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:398, in SymbolicExpression._evaluate_(self, sources)
    396     yield result
    397 else:
--> 398     for result in map(
    399         self._evaluate_conclusions_and_update_bindings_,
    400         self._evaluate__(sources),
    401     ):
    402         evaluation_context.on_result_yielded(expression=self, result=result)
    403         yield result

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/variable.py:263, in InstantiatedVariable._evaluate__(self, sources)
    259 def _evaluate__(
    260     self,
    261     sources: OperationResult,
    262 ) -> Iterable[OperationResult]:
--> 263     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:289, in InstantiatedVariable._instantiate_using_child_vars_and_yield_results_(self, sources)
    279 # A callable class (Predicate / SymbolicFunction) implements HasBoundValue -- it binds the
    280 # constructed instance, or, for a value operation, its constructed-and-called value -- the
    281 # class-form counterpart of a @symbolic_function being called. A plain function/type does
    282 # not, so it binds the direct call result.
    283 bind = (
    284     self._type_._bound_value_
    285     if inspect.isclass(self._type_)
    286     and issubclass(self._type_, HasBoundValue)
    287     else self._type_
    288 )
--> 289 instance = bind(**kwargs)
    291 bindings = {self._id_: instance} | child_result.bindings
    292 result = self._build_operation_result_and_update_truth_value_(
    293     bindings, child_result
    294 )

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#