polix.collection-ops
Extensible collection operator system for quantifiers and aggregations.
Collection operators define how policies traverse and aggregate over collections. This includes quantifiers like :forall and :exists, as well as aggregations like :count, :sum, :avg, etc.
Built-in Collection Operators
Quantifiers: :forall, :exists Aggregations: :count, :sum
Defining Custom Collection Operators
Use defcollectionop to define new operators:
(defcollectionop :every
:op-type :quantifier
:empty-result true
:init-state (fn [] {})
:process-element (fn [state _elem result _idx]
(if (false? result)
{:short-circuit false}
{:state state}))
:finalize (fn [_state residuals]
(if (empty? residuals) true {:residual residuals})))
Or use register-collection-op! for programmatic registration.
clear-registry!
(clear-registry!)Clears all registered collection operators. Useful for testing.
collection-op-keys
(collection-op-keys)Returns all registered collection operator keys.
defcollectionop
macro
(defcollectionop op-key & {:keys [op-type empty-result init-state process-element finalize can-merge? merge-bodies simplify-comparison]})Defines and registers a collection operator.
Example:
(defcollectionop :every
:op-type :quantifier
:empty-result true
:init-state (fn [] {})
:process-element (fn [state _elem result _idx]
(if (false? result)
{:short-circuit false}
{:state state}))
:finalize (fn [_state residuals]
(if (empty? residuals) true {:residual residuals})))
get-collection-op
(get-collection-op op-key)Returns the collection operator for op-key, or nil if not found.
ICollectionOp
protocol
Protocol for collection-based operations (quantifiers and aggregations).
Collection operators traverse collections with optional filtering and accumulate results using the protocol methods. The traversal function handles binding context, filter evaluation, and residual tracking.
members
empty-result
(empty-result this)Returns the result for an empty collection after filtering.
Examples: - :forall returns true (vacuous truth) - :exists returns false - :count returns 0 - :sum returns 0
finalize
(finalize this state residuals)Finalizes the accumulated state into the final result.
Called when all elements have been processed without short-circuit. The residuals map contains any residual constraints from elements that couldn’t be fully evaluated.
init-state
(init-state this)Returns the initial accumulator state for the operation.
The state is an arbitrary map that gets threaded through process-element calls and passed to finalize.
op-type
(op-type this)Returns the operator type: :quantifier or :aggregation.
Quantifiers evaluate a body predicate for each element (forall, exists). Aggregations extract values from elements (count, sum, avg).
process-element
(process-element this state elem-value body-result index)Processes a single element into the accumulator.
Takes: - state - current accumulator state - elem-value - the element itself (for aggregations like sum) - body-result - result of body predicate (for quantifiers) or filter - index - element index in collection
Returns one of: - {:state new-state} - continue with updated state - {:short-circuit result} - stop iteration and return result
ICollectionOpSimplify
protocol
Optional protocol for compile-time simplification of collection operators.
Implement this protocol to enable the compiler to optimize policies containing collection operators.
members
can-merge?
(can-merge? this other-op-key)Returns true if this operator can be merged with another on the same path.
Example: Two :forall operators on the same collection can be merged into a single forall with an AND-ed body.
merge-bodies
(merge-bodies this body1 body2)Merges two body ASTs when operators are combined.
For :forall, this typically means AND-ing the bodies. For :exists, this typically means OR-ing the bodies.
simplify-comparison
(simplify-comparison this comparison-op expected-value)Simplifies when the collection op result is used in a comparison.
Returns nil if no simplification is possible, otherwise returns a simplified AST node.
Example: [:> [:fn/count :doc/users] 0] can simplify to [:exists [_ :doc/users] true]
ICollectionOpTrace
protocol
Protocol for tracing collection operator evaluation.
Operators can optionally implement this protocol to provide detailed trace information during evaluation.
members
trace-element
(trace-element this ctx elem idx filter-result body-result)Called for each element processed. Appends element trace if enabled.
trace-end
(trace-end this ctx result trace-entry)Called when traversal completes. Finalizes trace entry.
trace-start
(trace-start this binding ctx)Called when traversal starts. Returns updated ctx with trace entry.
index-residual
(index-residual residual-result coll-path index)Transforms residual paths to include collection index.
Takes a residual result and prefixes all paths with the collection path and element index, e.g., {[:role] [...]} becomes {[:users 0 :role] [...]}.
merge-residual-paths
(merge-residual-paths acc residual-result)Merges residual path maps from multiple residual results.
register-builtins!
(register-builtins!)Registers all built-in collection operators.
register-collection-op!
(register-collection-op! op-key spec)Registers a collection operator in the global registry.
op-key is the operator keyword (e.g., :forall, :count).
spec is a map with required and optional keys: - :op-type - (required) :quantifier or :aggregation - :empty-result - (required) result for empty collection - :init-state - (required) (fn [] -> state) - :process-element - (required) (fn [state elem result idx] -> {:state ...} | {:short-circuit ...}) - :finalize - (required) (fn [state residuals] -> result) - :can-merge? - (optional) (fn [other-op-key] -> boolean) - :merge-bodies - (optional) (fn [body1 body2] -> merged-body) - :simplify-comparison - (optional) (fn [comp-op expected] -> ast | nil)
Throws if spec is invalid.
residual?
(residual? x)Returns true if x is a residual result.
resolve-collection
(resolve-collection binding document ctx get-binding-fn path-exists-fn)Resolves the collection for a quantifier binding.
Returns {:ok collection} if found, or {:missing path} if the collection path doesn’t exist, or {:invalid value} if the value is not sequential.
The get-binding-fn should be (fn [ctx name] -> value) for looking up bound variables in the evaluation context.
traverse-collection
(traverse-collection coll-op binding body document ctx {:keys [eval-ast-fn with-binding-fn get-binding-fn path-exists-fn]})Generic collection traversal for quantifiers and aggregations.
This is the core iteration function that handles: - Collection resolution from binding - Optional filter evaluation (:where clause) - Body evaluation (for quantifiers) - Binding context management - Residual tracking with indexed paths - Short-circuit returns - Tracing
Parameters: - coll-op - the collection operator (implements ICollectionOp) - binding - binding map with :namespace, :path, :name, :where - body - body AST node (for quantifiers) or nil (for aggregations) - document - the document being evaluated - ctx - evaluation context with bindings and trace info - eval-ast-fn - (fn [ast document ctx] -> result) for evaluating AST - with-binding-fn - (fn [ctx name value] -> ctx) for adding bindings - get-binding-fn - (fn [ctx name] -> value) for getting bindings - path-exists-fn - (fn [doc path] -> boolean) for path existence check
Returns three-valued result: true, false, value, or {:residual …}.
validate-collection-op-spec!
(validate-collection-op-spec! op-key spec)Validates collection operator spec against schema, throws on invalid.