Macro Cookbook

This chapter is a quick map of the macro surface. The goal is not to replace the deeper chapters, but to make the everyday question easy:

"I know roughly what I want to do. Which macro should I reach for?"

The macros fall into three layers:

  • Schema definition: attributes!
  • Fact construction: entity!
  • Query construction: find!, exists!, pattern!, pattern_changes!, path!, and!, or!, temp!, ignore!

Define attributes with attributes!

Use attributes! to declare typed attributes once, then reuse them everywhere else.

#![allow(unused)]
fn main() {
use triblespace::prelude::*;

mod social {
    use triblespace::prelude::*;
    use triblespace::prelude::valueschemas::{GenId, ShortString};

    attributes! {
        /// A person's display name.
        "A74AA63539354CDA47F387A4C3A8D54C" as pub name: ShortString;

        /// Another person this person knows.
        pub friend: GenId;
    }
}

assert_ne!(social::name.id(), social::friend.id());
}

Reach for this macro when:

  • you are defining a namespace or schema module
  • you want attributes with stable ids and value schemas
  • you want doc comments to become attribute metadata

If you already have attributes, you usually do not need attributes! in the rest of the code you are writing.

Build facts with entity!

Use entity! when you want to create tribles for one entity.

#![allow(unused)]
fn main() {
use triblespace::prelude::*;
mod social {
    use triblespace::prelude::*;
    use triblespace::prelude::valueschemas::ShortString;
    attributes! {
        "A74AA63539354CDA47F387A4C3A8D54C" as pub name: ShortString;
    }
}
let alice = fucid();
let facts = entity! { &alice @
    social::name: "Alice",
};

assert_eq!(facts.root(), Some(alice.id));
assert_eq!(facts.len(), 1);
}

If you omit the entity id, entity! derives one deterministically from the attribute/value pairs.

#![allow(unused)]
fn main() {
use triblespace::prelude::*;
mod social {
    use triblespace::prelude::*;
    use triblespace::prelude::valueschemas::ShortString;
    attributes! {
        "A74AA63539354CDA47F387A4C3A8D54C" as pub name: ShortString;
    }
}
let facts = entity! { _
    @ social::name: "Alice"
};

assert!(facts.root().is_some());
assert_eq!(facts.len(), 1);
}

The macro also supports optional and repeated fields:

let aliases = ["Al", "A."];
let maybe_nickname = Some("Ace");

let facts = entity! { &alice @
    social::name: "Alice",
    social::nickname?: maybe_nickname,
    social::alias*: aliases,
};

Reach for this macro when:

  • you are constructing new data
  • you want optional/repeated attribute ergonomics
  • you want a deterministic derived id for a value object

Match facts with pattern!

Use pattern! to turn trible-shaped structure into a query constraint.

#![allow(unused)]
fn main() {
use triblespace::prelude::*;
mod social {
    use triblespace::prelude::*;
    use triblespace::prelude::valueschemas::{GenId, ShortString};
    attributes! {
        "A74AA63539354CDA47F387A4C3A8D54C" as pub name: ShortString;
        "B74AA63539354CDA47F387A4C3A8D54C" as pub friend: GenId;
    }
}
let mut kb = TribleSet::new();
let alice = fucid();
let bob = fucid();
kb += entity! { &alice @ social::friend: &bob };
kb += entity! { &bob @ social::name: "Bob" };
let results: Vec<Id> = find!(
    friend: Id,
    pattern!(&kb, [
        { alice.id @ social::friend: ?friend },
        { ?friend @ social::name: "Bob" }
    ])
).collect();

assert_eq!(results, vec![bob.id]);
}

Inside a pattern:

  • ?name refers to a query variable from the surrounding query
  • _?name introduces a local helper variable scoped to that pattern
  • literal expressions become equality constraints automatically

Use pattern! when you are querying the current contents of a TribleSet, Checkout, or another pattern-capable source.

Query for results with find!

Use find! when you want rows back.

#![allow(unused)]
fn main() {
use triblespace::prelude::*;
mod social {
    use triblespace::prelude::*;
    use triblespace::prelude::valueschemas::ShortString;
    attributes! {
        "A74AA63539354CDA47F387A4C3A8D54C" as pub name: ShortString;
    }
}
let mut kb = TribleSet::new();
let alice = fucid();
kb += entity! { &alice @ social::name: "Alice" };
let names: Vec<Value<_>> = find!(
    name: Value<_>,
    pattern!(&kb, [{ _?person @ social::name: ?name }])
).collect();

let first: &str = names[0].try_from_value().unwrap();
assert_eq!(first, "Alice");
}

There are three common shapes:

  • find!(value, constraint) for one projected variable as a bare value
  • find!((a, b), constraint) for tuples
  • find!((), constraint) when you care only that matches exist

Typed projections happen in the head:

#![allow(unused)]
fn main() {
use triblespace::prelude::*;
mod social {
    use triblespace::prelude::*;
    use triblespace::prelude::valueschemas::ShortString;
    attributes! {
        "A74AA63539354CDA47F387A4C3A8D54C" as pub name: ShortString;
    }
}
let mut kb = TribleSet::new();
let alice = fucid();
kb += entity! { &alice @ social::name: "Alice" };
let ids: Vec<_> = find!(
    person: Id,
    pattern!(&kb, [{ ?person @ social::name: "Alice" }])
).collect();

assert_eq!(ids, vec![alice.id]);
}

Use ? on a projected variable when you want conversion failures as Result<T, E> instead of dropping the row.

Ask existence questions with exists!

Use exists! when you only need yes/no.

#![allow(unused)]
fn main() {
use triblespace::prelude::*;
mod social {
    use triblespace::prelude::*;
    use triblespace::prelude::valueschemas::ShortString;
    attributes! {
        "A74AA63539354CDA47F387A4C3A8D54C" as pub name: ShortString;
    }
}
let mut kb = TribleSet::new();
let bob = fucid();
kb += entity! { &bob @ social::name: "Bob" };
let has_bob = exists!(
    pattern!(&kb, [{ _?person @ social::name: "Bob" }])
);

assert!(has_bob);
}

You can also keep the typed-head form when the projection itself matters to the check:

let has_name = exists!(
    (name: Value<_>),
    pattern!(&kb, [{ ?person @ social::name: ?name }])
);

Use exists!(constraint) for pure existence checks instead of find!((), constraint).next().is_some().

Match only new results with pattern_changes!

Use pattern_changes! for incremental queries: matches are allowed to join against the full current state, but at least one contributing trible must come from the change set.

for (work,) in find!(
    (work: Value<_>),
    pattern_changes!(&full, &delta, [
        { ?work @ literature::author: &shakespeare }
    ])
) {
    // process only newly introduced matches
}

Reach for this macro when:

  • you already have full and delta
  • you want monotonic incremental processing
  • pattern! would re-emit old matches every time

See Incremental Queries for the full workflow.

Traverse edges with path!

Use path! when a relationship is recursive or variable-length.

#![allow(unused)]
fn main() {
use triblespace::prelude::*;
mod social {
    use triblespace::prelude::*;
    attributes! {
        "B74AA63539354CDA47F387A4C3A8D54C" as pub friend: valueschemas::GenId;
    }
}
let mut kb = TribleSet::new();
let alice = fucid();
let bob = fucid();
let carol = fucid();
kb += entity! { &alice @ social::friend: &bob };
kb += entity! { &bob @ social::friend: &carol };
let results: Vec<(Id, Id)> = find!(
    (src: Id, dst: Id),
    path!(kb.clone(), src social::friend+ dst)
).collect();

assert!(results.contains(&(alice.id, bob.id)));
assert!(results.contains(&(alice.id, carol.id)));
assert!(results.contains(&(bob.id, carol.id)));
}

The middle part is a small regular language over attributes:

  • adjacency means concatenation
  • | means alternation
  • * means zero or more
  • + means one or more
  • parentheses group

Example:

path!(kb.clone(), start (social::friend | social::colleague)+ end)

Use path! when a fixed number of pattern! clauses would be awkward or impossible.

Combine constraints with and! and or!

Use and! when every clause must hold:

find!(
    (friend: Value<_>),
    and!(
        pattern!(&kb, [{ ?person @ social::name: "Alice" }]),
        pattern!(&kb, [{ ?person @ social::friend: ?friend }])
    )
)

Use or! when any branch may hold:

find!(
    (alias: Value<_>),
    or!(
        pattern!(&kb, [{ ?person @ social::nickname: ?alias }]),
        pattern!(&kb, [{ ?person @ social::name: ?alias }])
    )
)

or! branches must mention the same variable set.

Introduce helpers with temp!

Use temp! when you need a fresh variable only inside a sub-expression.

find!(
    (person: Value<_>),
    temp!((friend), and!(
        pattern!(&kb, [{ ?person @ social::friend: ?friend }]),
        pattern!(&kb, [{ ?friend @ social::name: "Bob" }])
    ))
)

This is useful when the helper participates in joins but should not be projected.

Hide helpers with ignore!

Use ignore! when a constraint needs internal variables that should not be visible to the outer planner.

find!(
    (person: Value<_>),
    ignore!((friend),
        pattern!(&kb, [
            { ?person @ social::friend: ?friend },
            { ?friend @ social::name: "Bob" }
        ])
    )
)

This is a more specialized tool than temp!. Reach for it when you already have a nested constraint that uses helper variables internally and you want to hide them from the outer query.

Which macro should I use?

If you are:

  • defining a schema: use attributes!
  • building facts for one entity: use entity!
  • matching trible structure: use pattern!
  • matching only newly added results: use pattern_changes!
  • asking for rows back: use find!
  • asking for a boolean: use exists!
  • traversing recursive edges: use path!
  • requiring all clauses: use and!
  • allowing alternatives: use or!
  • introducing a fresh helper variable: use temp!
  • hiding helper variables from the outer query: use ignore!

From here, the best next stops are: