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