Skip to content

Functors

Functors let you parameterize predicates: take an existing rule and substitute one of the predicates it depends on, producing a new predicate.

NewPredicate := FunctorPredicate(Arg1: Value1, Arg2: Value2);

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)