Skip to content

Programs you can compose

MeTTa's own module system is file-based: import! resolves a path and binds the loaded space to a token. That is the right thing for MeTTa source and the wrong shape for TypeScript, where the unit you want to pass around is a value.

A module here records what to add and carries the type of what it declares.

ts
import { 
mettaDB
,
mettaModule
,
mul
,
names
,
vars
} from "@mettascript/edsl";
const {
Likes
} =
names
("Likes");
const {
drink
} =
vars
("drink");
const
arith
=
mettaModule
()
.
grounded
("double", (
n
: number) =>
n
* 2)
.
define
("quad", ["Number"], "Number", (
self
,
n
) =>
mul
(
n
, 4));
const
data
=
mettaModule
().
relation
("Likes", ["String", "String"]).
atoms
([
Likes
, "Ada", "Coffee"]);
const
db
=
mettaDB
().
use
(
arith
.
use
(
data
));
db
.
call
.
quad
(3); // [12], typed number[]
db
.
query
([
Likes
, "Ada",
drink
]); // [{ drink: "Coffee" }], typed { drink: string }[]

Using a module teaches the runner everything in it, so nothing is declared twice. The shape is tRPC's router merge: sub-routers are built independently, merged at a root, and the merged type carries every branch. use composes both ways, so arith.use(data) is a module and db.use(...) is a runner.

Applying happens once per module per runner, tracked by identity. A module applied twice would add its rules twice, and in MeTTa two clauses means the call answers twice.

Signatures are values

["Number"], "Number" is a value, not a type argument. That is Zod's and tRPC's move, and here it pays three ways at once.

TypeScript cannot infer one type argument while you supply another, so a type argument would force you to spell the function's name twice. The same list emits (: quad (-> Number Number)), which teaches the engine the type as well, something a TypeScript type can never do. And it is the vocabulary relation already uses, so there is one way to say what a thing's type is.

Carrying MeTTa source

A module is a file in MeTTa, so source lets one carry raw text. Its !-queries run when the module is used, and that is the only route by which pragma! or bind! can reach a module at all, since neither is an atom to add.

ts
const 
lib
=
mettaModule
()
.
source
(`(= (triple $x) (* 3 $x))\n!(bind! &limit 10)`)
.
declare
("triple", ["Number"], "Number");
mettaDB
().
use
(
lib
).
call
.
triple
(7); // [21], typed number[]

declare states a function's type without defining it. Reach for it whenever source already defines the function: define would add a second clause, and MeTTa would then answer twice.

Emitting the declarations

m.declarations() hands back (: Name (-> ...)) for everything the module declares, relations ending in Type and functions in their return type.

ts
data
.
declarations
().
map
(
String
); // ['(: Likes (-> String String Type))']

Nothing is emitted until you ask, for the same reason declareRelations is opt-in: declaring is safe, and enforcing rejects programs that used to run. Using a module never changes evaluation on its own.

Released under the MIT License.