@@ -189,6 +189,79 @@ Then you can create rules of the following format:
189189 The annotation function will be called when all clauses in the rule have been satisfied and the head of the rule is to be annotated.
190190The ``annotations `` parameter in the annotation function will contain the bounds of the grounded atoms for each of the 4 clauses in the rule.
191191
192+ Head Functions and Functional Arguments
193+ ---------------------------------------
194+
195+ PyReason also supports applying user-defined functions directly within the **arguments of a rule head **. These head functions make it
196+ possible to transform the node or edge identifiers that will receive the rule's annotation. Common use cases include normalising IDs,
197+ mapping relationships, or selecting a subset of grounded nodes before the head is produced.
198+
199+ Registering a Head Function
200+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
201+
202+ Head functions, just like annotation functions, must be compiled with ``numba.njit `` and registered with PyReason before they can be
203+ used by rules. A head function receives a list of grounded variable bindings (each binding is a ``numba.typed.List `` of strings) and
204+ returns a new list containing the head substitutions that should be produced for that argument.
205+
206+ .. code-block :: python
207+
208+ import numba
209+ import pyreason as pr
210+
211+ @numba.njit
212+ def identity_func (groundings ):
213+ """
214+ Return the same grounded values that were passed in.
215+ `groundings` is a list where each element is the list of nodes bound to a variable.
216+ For example, if the rule body binds ``X`` to ``[a, b]`` and ``Y`` to ``[c, d]``,
217+ then ``groundings`` will be ``[[a, b], [c, d]]``.
218+ """
219+ return numba.typed.List([groundings[0 ][0 ]])
220+
221+ pr.add_head_function(identity_func)
222+
223+ Using Functions in the Rule Head
224+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
225+
226+ Once registered, a head function can be referenced in the textual rule by replacing a head argument with a function call. The parser
227+ automatically rewrites the argument to use an internal placeholder variable and associates the call with the registered function.
228+
229+ .. code-block :: text
230+
231+ Processed(identity_func(X)) <- property(X), property(Y), connected(X, Y)
232+
233+ During grounding, the rule body binds ``X `` and ``Y `` exactly as usual. Before emitting the head atom, PyReason invokes
234+ ``identity_func `` with the grounded values for ``X `` (and any additional arguments if the function requires them). The function's return
235+ value determines which nodes (or edges) will receive the ``Processed `` label.
236+
237+ Edge Rules with Head Functions
238+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
239+
240+ Head functions may be used for either argument of an edge rule. Each head argument is handled independently, so you can transform the
241+ source, the target, or both.
242+
243+ .. code-block :: text
244+
245+ Route(identity_func(A), B) <- property(X), property(Y), connected(X, Y)
246+ Path(A, identity_func(B)) <- property(X), property(Y), connected(X, Y)
247+ Link(identity_func(A), identity_func(B)) <- property(X), property(Y), connected(X, Y)
248+
249+ In the example above, every head argument that uses ``identity_func `` will receive the transformed grounding returned by the function.
250+ If both arguments reference a function, each function call is resolved separately before the head edge is emitted.
251+
252+ Guidelines and Best Practices
253+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
254+
255+ * Head functions must be decorated with ``@numba.njit `` so that they can execute inside PyReason's JIT-compiled reasoning loop.
256+ * Each argument supplied to the function corresponds to a grounded variable from the rule body; that argument is represented as a
257+ ``numba.typed.List `` of strings containing all candidate nodes for that variable.
258+ * The function must return a ``numba.typed.List `` of strings that represent the substituted values for the head argument.
259+ * Multiple head functions can be registered, and you can mix plain variables and function calls within the same rule head.
260+ * If you need the original grounding unchanged, simply return it from the function (as shown in ``identity_func ``).
261+
262+ By leveraging head functions you can encapsulate common transformations and keep your rule text concise while still benefitting from
263+ PyReason's compiled execution path.
264+
192265
193266Custom Thresholds
194267-----------------
0 commit comments