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