Skip to content

Abstract Terms

Kevin Giovinazzo edited this page Dec 6, 2023 · 8 revisions

In Ergo, in addition to the standard Prolog terms—atoms, variables, and complex terms—there exists a specialized term type known as abstract. These are designed for extending canonical representations by encapsulating additional semantics. Ergo uses abstract terms to implement some advanced data structures: Lists, Sets, Tuples, and Dictionaries. Each of these types supports two syntactic variations: the canonical form and a syntactic sugar form, which unify mutually.

Abstract terms themselves need to implement the AbstractTerm base class, or extend one of the base abstract term types: AbstractList, List, Set, NTuple and Dict. This makes them first-class ITerms on par with atoms, variables and complex terms, so you can use the same kind of pattern matching to test their type, except that they also benefit from inheritance.

Parsing

The parser for an abstract term needs to implement the IAbstractTermParser<T> interface, where T is the type of the abstract term.

This interface exposes an int ParsePriority which is important when implementing types that extend other abstract types, because Ergo implements a top-down backtracking parser that needs to evaluate the most exhaustive path first. Negative values will make the custom parser run before the built-in ones. Positive values will make the custom parser run after.

That parser should be registered through the ErgoFacade that's being used to set up the Ergo environment.

Expansion

AbstractTerm has a virtual method named Expand. This method implements term expansion for abstract terms automatically. The way it works is by expanding the canonical form of the abstract term, representing it as a string, and then parsing that string with the abstract term's own parser. This ensures that abstract terms don't decay back to regular terms.

Examples

A direct example comes from my game engine, where I implemented game entities as abstract terms that extend dictionaries. Doing so allows me to specify them only by their type and id, while populating the underlying dictionary with all their properties automatically and with fresh data -- keeping them updated through unification. This is not idiomatic Prolog, but it works well in the context of a specialized DSL for games.

An example would be:

X = actor{ id: 1 }
X/actor{id: 1, info: info_component{name: 'Bob'}, physics: {position: p(2, 4)}, actor: {type: player }}}
% Cast to physical_entity, which is more abstract than actor
Y = physical_entity{ id: 1 }
Y/physical_entity{ id: 1, info: info_component{name: 'Bob'}, physics: {position: p(2, 4)}}

The syntax is much more powerful than that, however. Combined with expansions, it allows me to write macros such as:

:- expand([O] >> (
    @player :- player as actor is O
)).

Which are then used like this:

% Pattern matching the result of the expansion, which will only match the player's turn
action:actor_turn_started(_{actor: @player}) :- 
	do_stuff.

% Obtaining the most recent value of a property after changing its value
_X = @player,
P1 = _X.physics.position,
move(_X, p(0, 0)),
P2 = _X.physics.position.
P1/p(10, 5) ; P2/p(0, 0)
Clone this wiki locally