Functors¶
Functors let you parameterize predicates: take an existing rule and substitute one of the predicates it depends on, producing a new predicate.
Reusing logic across segments¶
Define a generic pattern once, then instantiate it for different inputs:
# Define a reusable pattern
@OrderBy(SegmentRevenue, "segment_id");
SegmentRevenue(segment_id:, total? += amount) distinct :-
Segment(segment_id:, user_id:),
Orders(user_id:, amount:);
# Apply to different segments
EnterpriseRevenue := SegmentRevenue(Segment: EnterpriseCustomers);
SMBRevenue := SegmentRevenue(Segment: SMBCustomers);
SegmentRevenue depends on a predicate named Segment; each functor application replaces Segment with a concrete predicate and yields a new, independent rule.
The filter pattern¶
A common use is parameterized filtering: write a generic rule with a Filter dependency whose default matches all rows, then override the filter per query:
@OrderBy(CustomerRevenue, "customer_name");
CustomerRevenue(customer_name:, revenue? += amount) distinct :-
Filter(customer_name:), Orders(customer_name:, amount:);
# Default filter = all rows
Filter(customer_name:) distinct :- Orders(customer_name:);
# Specialized: only John
JohnFilter(customer_name: "John");
JohnsRevenue := CustomerRevenue(Filter: JohnFilter);
This keeps the aggregation logic in one place while allowing any number of filtered variants — ideal for ephemeral, per-question queries layered on top of a stable rule base.
Complete example¶
Both patterns together — the filter pattern (JohnsRevenue) and segment parameterization (EnterpriseRevenue, SMBRevenue):
# run: CustomerRevenue, JohnsRevenue, EnterpriseRevenue, SMBRevenue
@Engine("duckdb");
# Tables
Orders(customer_name: "John", user_id: 1, amount: 100);
Orders(customer_name: "John", user_id: 1, amount: 250);
Orders(customer_name: "Mary", user_id: 2, amount: 4000);
Orders(customer_name: "Acme", user_id: 3, amount: 12000);
EnterpriseCustomers(segment_id: "enterprise", user_id: 3);
SMBCustomers(segment_id: "smb", user_id: 1);
SMBCustomers(segment_id: "smb", user_id: 2);
# Rules
## The filter pattern: a generic rule with a Filter dependency.
@OrderBy(CustomerRevenue, "customer_name");
CustomerRevenue(customer_name:, revenue? += amount) distinct :-
Filter(customer_name:), Orders(customer_name:, amount:);
## Default filter = all rows.
Filter(customer_name:) distinct :- Orders(customer_name:);
## Specialized filter, applied with a functor.
JohnFilter(customer_name: "John");
JohnsRevenue := CustomerRevenue(Filter: JohnFilter);
## A reusable revenue pattern, parameterized by segment.
@OrderBy(SegmentRevenue, "segment_id");
SegmentRevenue(segment_id:, total? += amount) distinct :-
Segment(segment_id:, user_id:),
Orders(user_id:, amount:);
Segment(segment_id:, user_id:) :- EnterpriseCustomers(segment_id:, user_id:);
EnterpriseRevenue := SegmentRevenue(Segment: EnterpriseCustomers);
SMBRevenue := SegmentRevenue(Segment: SMBCustomers);
Generated SQL and execution results
$ synalog.check('functors.l')
No errors found.
$ synalog.compile('functors.l', 'CustomerRevenue')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;
WITH t_2_Orders AS (SELECT * FROM (
SELECT
E'John' AS customer_name,
1 AS user_id,
100 AS amount
UNION ALL
SELECT
E'John' AS customer_name,
1 AS user_id,
250 AS amount
UNION ALL
SELECT
E'Mary' AS customer_name,
2 AS user_id,
4000 AS amount
UNION ALL
SELECT
E'Acme' AS customer_name,
3 AS user_id,
12000 AS amount
) AS UNUSED_TABLE_NAME ),
t_0_Filter AS (SELECT
t_1_Orders.customer_name AS customer_name
FROM
t_2_Orders AS t_1_Orders
GROUP BY t_1_Orders.customer_name)
SELECT
Filter.customer_name AS customer_name,
SUM(Orders.amount) AS revenue
FROM
t_0_Filter AS Filter, t_2_Orders AS Orders
WHERE
(Orders.customer_name = Filter.customer_name)
GROUP BY Filter.customer_name ORDER BY customer_name;
-- Executed on DuckDB:
| customer_name | revenue |
|---------------|---------|
| Acme | 12000 |
| John | 350 |
| Mary | 4000 |
(3 rows)
$ synalog.compile('functors.l', 'JohnsRevenue')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;
WITH t_0_Orders AS (SELECT * FROM (
SELECT
E'John' AS customer_name,
1 AS user_id,
100 AS amount
UNION ALL
SELECT
E'John' AS customer_name,
1 AS user_id,
250 AS amount
UNION ALL
SELECT
E'Mary' AS customer_name,
2 AS user_id,
4000 AS amount
UNION ALL
SELECT
E'Acme' AS customer_name,
3 AS user_id,
12000 AS amount
) AS UNUSED_TABLE_NAME )
SELECT
Orders.customer_name AS customer_name,
SUM(Orders.amount) AS revenue
FROM
t_0_Orders AS Orders
WHERE
(E'John' = Orders.customer_name)
GROUP BY Orders.customer_name ORDER BY customer_name;
-- Executed on DuckDB:
| customer_name | revenue |
|---------------|---------|
| John | 350 |
(1 row)
$ synalog.compile('functors.l', 'EnterpriseRevenue')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;
WITH t_0_Orders AS (SELECT * FROM (
SELECT
E'John' AS customer_name,
1 AS user_id,
100 AS amount
UNION ALL
SELECT
E'John' AS customer_name,
1 AS user_id,
250 AS amount
UNION ALL
SELECT
E'Mary' AS customer_name,
2 AS user_id,
4000 AS amount
UNION ALL
SELECT
E'Acme' AS customer_name,
3 AS user_id,
12000 AS amount
) AS UNUSED_TABLE_NAME )
SELECT
E'enterprise' AS segment_id,
SUM(Orders.amount) AS total
FROM
t_0_Orders AS Orders
WHERE
(Orders.user_id = 3)
GROUP BY (E'enterprise' || '') ORDER BY segment_id;
-- Executed on DuckDB:
| segment_id | total |
|------------|-------|
| enterprise | 12000 |
(1 row)
$ synalog.compile('functors.l', 'SMBRevenue')
-- Initializing DuckDB environment.
create schema if not exists logica_home;
-- Empty record, has to have a field by DuckDB syntax.
drop type if exists logicarecord893574736 cascade; create type logicarecord893574736 as struct(nirvana numeric);
create sequence if not exists eternal_logical_sequence;
WITH t_0_SMBCustomers AS (SELECT * FROM (
SELECT
E'smb' AS segment_id,
1 AS user_id
UNION ALL
SELECT
E'smb' AS segment_id,
2 AS user_id
) AS UNUSED_TABLE_NAME ),
t_1_Orders AS (SELECT * FROM (
SELECT
E'John' AS customer_name,
1 AS user_id,
100 AS amount
UNION ALL
SELECT
E'John' AS customer_name,
1 AS user_id,
250 AS amount
UNION ALL
SELECT
E'Mary' AS customer_name,
2 AS user_id,
4000 AS amount
UNION ALL
SELECT
E'Acme' AS customer_name,
3 AS user_id,
12000 AS amount
) AS UNUSED_TABLE_NAME )
SELECT
SMBCustomers.segment_id AS segment_id,
SUM(Orders.amount) AS total
FROM
t_0_SMBCustomers AS SMBCustomers, t_1_Orders AS Orders
WHERE
(Orders.user_id = SMBCustomers.user_id)
GROUP BY SMBCustomers.segment_id ORDER BY segment_id;
-- Executed on DuckDB:
| segment_id | total |
|------------|-------|
| smb | 4350 |
(1 row)