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:149, in SelectiveBackend.evaluate(self, expression)
    147     raise SelectiveBackendCannotResolveEllipsisMatch(expression)
    148 self._warn_or_raise_on_unresolved_cause_(expression)
--> 149 yield from self._evaluate(expression)

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

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

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/backends.py:205, in EntityQueryLanguageBackend._evaluate(self, expression)
    204 def _evaluate(self, expression: Evaluable) -> Iterable:
--> 205     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:328, in SymbolicExpression.evaluate(self, backend)
    326 SymbolGraph().remove_dead_instances()
    327 results = (self._process_result_(res) for res in self._true_results_())
--> 328 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:327, in <genexpr>(.0)
    318 """
    319 Evaluate the query and map the results to the correct output data structure.
    320 This is the exposed evaluation method for users.
   (...)    324     argument.
    325 """
    326 SymbolGraph().remove_dead_instances()
--> 327 results = (self._process_result_(res) for res in self._true_results_())
    328 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:350, in <genexpr>(.0)
    344 else:
    345     results = evaluation_context.iterate_as_current(
    346         self._evaluate_within_(evaluation_context)
    347     )
    348 return (
    349     result
--> 350     for result in results
    351     if not isinstance(self, TruthValuedExpression) or result.is_true
    352 )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:574, in SymbolicExpression._create_new_context_and_evaluate_(self, sources)
    560 """
    561 Start a new evaluation of this expression, independent of any evaluation already
    562 running, such as one that reaches this call through a lazily iterated domain.
   (...)    568 :return: An iterator of OperationResult instances.
    569 """
    570 from krrood.entity_query_language.evaluation import (
    571     create_default_evaluation_context,
    572 )
--> 574 yield from self._evaluate_in_new_context_(
    575     create_default_evaluation_context(), sources
    576 )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:594, in SymbolicExpression._evaluate_in_new_context_(self, evaluation_context, sources)
    583 """
    584 Start an evaluation of this expression in a context no evaluation has used yet.
    585 
   (...)    589 :return: An iterator of OperationResult instances.
    590 """
    591 evaluation_context.active_conditions_root.set_active_root_if_not_set(
    592     self._conditions_root_, has_condition=self._has_condition_
    593 )
--> 594 yield from evaluation_context.iterate_as_current(
    595     self._evaluate_within_(evaluation_context, sources)
    596 )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/evaluation_context.py:389, in EvaluationContext.iterate_as_current(self, results)
    387 while True:
    388     with self.as_current():
--> 389         result = next(results, results_exhausted)
    390     if result is results_exhausted:
    391         return

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:627, in SymbolicExpression._evaluate_within_(self, evaluation_context, sources)
    625     yield result
    626 else:
--> 627     for result in map(
    628         self._evaluate_conclusions_and_update_bindings_,
    629         self._evaluate__(sources),
    630     ):
    631         evaluation_context.on_result_yielded(expression=self, result=result)
    632         yield result

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/query.py:668, in Query._evaluate__(self, sources)
    664 evaluation_context = get_evaluation_context()
    665 if evaluation_context is None or not self._is_nested_subquery_(
    666     evaluation_context
    667 ):
--> 668     yield from self._produce_results_(sources)
    669     return
    671 cached_stream = evaluation_context.subquery_result_cache.get_or_create(
    672     self._id_,
    673     lambda: CachedResultStream(self._produce_results_(OperationResult({}))),
    674 )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/query.py:701, in Query._produce_results_(self, sources)
    699 for transformer in self._result_transformers_:
    700     results = transformer.transform(results)
--> 701 yield from results
    703 if self._seen_results is not None:
    704     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:695, in <genexpr>(.0)
    686 def _produce_results_(self, sources: OperationResult) -> Iterator[OperationResult]:
    687     """
    688     Produce this product's results: the projected, result-mapped rows of its cartesian product.
    689 
    690     :param sources: The current bindings.
    691     :return: An iterator over the query's result rows.
    692     """
    693     results = (
    694         self._get_operation_result_(result)
--> 695         for result in self._apply_results_mapping_(
    696             self._evaluate_product_(sources),
    697         )
    698     )
    699     for transformer in self._result_transformers_:
    700         results = transformer.transform(results)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/utils.py:149, in cartesian_product_while_passing_the_bindings_around(expressions, sources)
    143     return stage
    145 expression_evaluation_generators = [
    146     _make_stage(expression) for expression in expressions
    147 ]
--> 149 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:189, in chain_stages(stages, initial)
    186     for next_result in stages[stage_index](current_result):
    187         yield from evaluate_next_stage_or_yield(stage_index + 1, next_result)
--> 189 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:186, in chain_stages.<locals>.evaluate_next_stage_or_yield(stage_index, current_result)
    184     yield current_result
    185     return
--> 186 for next_result in stages[stage_index](current_result):
    187     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:138, in cartesian_product_while_passing_the_bindings_around.<locals>._make_stage.<locals>.stage(prev)
    133 def stage(prev: OperationResult | None) -> Iterator[OperationResult]:
    134     """
    135     Evaluate the inner expression and combine its bindings with the previous
    136     stage's bindings.
    137     """
--> 138     for result in inner_expression._evaluate_(prev):
    139         if prev is not None:
    140             result.update(prev)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:551, in SymbolicExpression._evaluate_(self, sources)
    549 evaluation_context = get_evaluation_context()
    550 if evaluation_context is not None:
--> 551     yield from self._evaluate_within_(evaluation_context, sources)
    552     return
    554 yield from self._create_new_context_and_evaluate_(sources)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:627, in SymbolicExpression._evaluate_within_(self, evaluation_context, sources)
    625     yield result
    626 else:
--> 627     for result in map(
    628         self._evaluate_conclusions_and_update_bindings_,
    629         self._evaluate__(sources),
    630     ):
    631         evaluation_context.on_result_yielded(expression=self, result=result)
    632         yield result

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/operations.py:61, in Where._evaluate__(self, sources)
     60 def _evaluate__(self, sources: OperationResult) -> Iterator[OperationResult]:
---> 61     yield from (
     62         result
     63         for result in self._evaluate_child_as_condition_(self._child_, sources)
     64         if result.is_true
     65     )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/query/operations.py:63, in <genexpr>(.0)
     60 def _evaluate__(self, sources: OperationResult) -> Iterator[OperationResult]:
     61     yield from (
     62         result
---> 63         for result in self._evaluate_child_as_condition_(self._child_, sources)
     64         if result.is_true
     65     )

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:1291, in TruthValueOperator._evaluate_child_as_condition_(self, child, sources)
   1289 if evaluation_context is not None:
   1290     evaluation_context.truth_value_operator_children.record(child._id_)
-> 1291 for result in child._evaluate_(sources):
   1292     if result.has_value:
   1293         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:551, in SymbolicExpression._evaluate_(self, sources)
    549 evaluation_context = get_evaluation_context()
    550 if evaluation_context is not None:
--> 551     yield from self._evaluate_within_(evaluation_context, sources)
    552     return
    554 yield from self._create_new_context_and_evaluate_(sources)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/base_expressions.py:627, in SymbolicExpression._evaluate_within_(self, evaluation_context, sources)
    625     yield result
    626 else:
--> 627     for result in map(
    628         self._evaluate_conclusions_and_update_bindings_,
    629         self._evaluate__(sources),
    630     ):
    631         evaluation_context.on_result_yielded(expression=self, result=result)
    632         yield result

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/variable.py:237, in InstantiatedVariable._evaluate__(self, sources)
    233 def _evaluate__(
    234     self,
    235     sources: OperationResult,
    236 ) -> Iterable[OperationResult]:
--> 237     yield from self._instantiate_using_child_variables_and_yield_results_(sources)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/core/variable.py:263, in InstantiatedVariable._instantiate_using_child_variables_and_yield_results_(self, sources)
    253 # A callable class (Predicate / SymbolicFunction) implements HasBoundValue -- it binds the
    254 # constructed instance, or, for a value operation, its constructed-and-called value -- the
    255 # class-form counterpart of a @symbolic_function being called. A plain function/type does
    256 # not, so it binds the direct call result.
    257 bind = (
    258     self._type_._bound_value_
    259     if inspect.isclass(self._type_)
    260     and issubclass(self._type_, HasBoundValue)
    261     else self._type_
    262 )
--> 263 instance = bind(**arguments)
    265 bindings = {self._id_: instance} | child_result.bindings
    266 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:272, in SymbolicCallable._bound_value_(cls, **kwargs)
    263 @classmethod
    264 def _bound_value_(cls, **kwargs) -> Any:
    265     """:return: the value this operation contributes to a query result when its arguments have
    266     concrete values -- the constructed instance itself by default (a :class:`Predicate`'s truth is
    267     then read from that instance). A value operation overrides this to its COMPUTED value.
   (...)    270         the query binds ``function(**values)``; for a callable class it binds this.
    271     """
--> 272     return cls._construct_normally_(**kwargs)

File ~/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/predicate.py:259, in SymbolicCallable._construct_normally_(cls, **kwargs)
    241 @classmethod
    242 def _construct_normally_(cls, **kwargs) -> SymbolicCallable:
    243     """
    244     Construct a concrete instance directly, bypassing the symbolic ``__new__``
    245     redirect.
   (...)    257     contain.
    258     """
--> 259     instance = object.__new__(cls)
    260     instance.__init__(**kwargs)
    261     return instance

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

API Reference#