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_'