LogiQL is a typed language and few typed languages are useful without the ability to create custom types. Like most things in LogiQL, types are represented as predicates. For instance, there is a
string predicate for textual values, and an
int predicate for numeric values. In our “LogiQL in 30 minutes” tutorial we represented ice creams as strings to keep things simple:
//lang:logiql cost[icecream] = c -> string(icecream), int(c). price[icecream] = p -> string(icecream), int(p).
This works, but does not take full advantage of the LogiQL type system. As your application grows, how do you keep track of all those things encoded as strings and integers? Wouldn’t it be much nicer to just create an
icecream type so that we all understand what we’re talking about — and likely save space in our workspace because we don’t have to duplicate those string values all over the place?
For this reason LogiQL supports entities which are a way to define custom types. In its most basic form, defining an entity is as simple as:
//lang:logiql icecream(ic) -> .
This states: “there exists such a thing as icecream.” However, to be able to actually create instances of ice creams, we need to define either a reference mode predicate, or one or more constructor predicates.
Let’s start with a reference mode (or refmode for short) example:
//lang:logiql icecream(ic), icecream_name(ic:name) -> string(name).
In a single clause, the
icecream_name predicates define the
icecream entity and its refmode predicate. Note the
ic:name syntax, which indicates we’re defining a refmode. We can now use our refmode predicate to create instances of the
icecream entity. For instance:
//lang:logiql +icecream(ic), +icecream_name[ic] = "Popsicle Lemon".
which states: “There exists an icecream
ic, and the name of this icecream
ic is ‘Popsicle Lemon.'”
We can now use our
icecream entity in our
//lang:logiql cost[icecream] = c -> icecream(icecream), int(c). price[icecream] = p -> icecream(icecream), int(p).
However, a naming convention that is recommended is to clearly indicate that these predicates can be seen as attributes of the
icecream entity by naming it along the lines of
//lang:logiql icecream_cost[icecream] = c -> icecream(icecream), int(c). icecream_price[icecream] = p -> icecream(icecream), int(p).
We can now create instances of ice cream and set their price and cost as follows:
//lang:logiql +icecream(ic), +icecream_name[ic] = "Popsicle Lemon", +icecream_cost[ic] = 25, +icecream_price[ic] = 50.
Note the use of commas to make sure this is one clause and all
ic‘s refer to the same
For convenience, it’s possible to use the value of the refmode predicate in place of actual entity values. The LogicBlox compiler will automatically figure out what is intended. In addition, any refmode that did not yet exist will automatically be created. So we can replace the previous four lines with:
//lang:logiql +icecream_cost["Popsicle Lemon"] = 25. +icecream_price["Popsicle Lemon"] = 50.
As a result, a fact will automatically be added to
icecream_name with a new entity instance for the “Popsicle Lemon” name.
Sidebar: Table versus Key-Value Modeling
Previously, we have stated that predicates can be thought of as tables in a SQL database. This is true, but does not accurately represent how predicates are typically used in LogicBlox applications. Let’s illustrate this with an example.
People familiar with SQL databases may be tempted to create a predicate like this:
//lang:logiql icecream(name, cost, price) -> string(name), int(cost), int(price).
That is: use one predicate that keeps all information related to ice creams. This is the SQL table way.
While doing this is technically possible in LogiQL, it is not common practice in LogiQL programs. Instead, LogicBlox is best used as a key-value store. That is: by creating a predicate per “attribute”. In fact, this is what we have been doing thus far. We created a
price predicate, a
cost predicate, and now an
icecream_name predicate. An extra advantage of this approach, from a modeling perspective, is that way can create predicates with any numbers of arguments. For instance, we can model a predicate that tracks whether a type of ice cream is in stock as follows:
//lang:logiql icecream_in_stock(ic) -> icecream(ic).
Without the need to implement the more awkward:
//lang:logiql icecream_in_stock[ic] = val -> icecream(ic), bool(val).
Similarly, as we have seen for our
sales predicate, we can can use any number of keys to model things like “in week
week, the number of sales of ice cream type
ic in store
value” in a very natural way:
//lang:logiql sales[week, ic, store] = val -> week(week), icecream(ic), store(store), int(val).
To create multi-column primary keys you need to use constructors rather than refmodes. For instance, if you prefer to split out the ice cream type and taste and have the combination be the “primary key” of the entity. In this case constructors can be used as follows:
//lang:logiql icecream(ic) -> . icecream_from_type_taste[type, taste] = ic -> string(type), string(taste), icecream(ic). lang:constructor(`icecream_from_type_taste).
The first clause defines that
icecream is an entity (without a refmode — you cannot use both refmodes and constructors for a single entity). The second clause defines a predicates that maps the combination of an ice cream type and a taste to an
icecream entity value. The third clause defines that this
icecream_from_type_taste predicate is a constructor.
We can now make instances of our entity using:
//lang:logiql +icecream(ic), +icecream_from_type_taste["Popsicle", "Lemon"] = ic.
And set cost and price for the Popsicle Lemon ice cream using:
//lang:logiql +icecream_cost[icecream_from_type_taste["Popsicle", "Lemon"]] = 25. +icecream_price[icecream_from_type_taste["Popsicle", "Lemon"]] = 50.
LogiQL also supports the creation of entity type hierarchies. For instance, if at some point we may want to start selling different things than just ice creams, we can hedge our bets and start out with a
//lang:logiql sellable(s) -> .
We can then create a subtype of
sellable, simply by stating that if there exists an ice cream
ic there also exists a
//lang:logiql icecream(ic) -> sellable(ic).
In this case, however, we do need to also explicitly state that
icecream is an entity itself:
In the future we can define other sellable goods, like candy bars:
//lang:logiql candybar(cb) -> sellable(cb). lang:entity(`candybar).
New value types can be defined in LogiQL using entities, which support more descriptive data models by referring to the types by name rather than some primitive type representation of the value. LogiQL has two ways to support creating entity values: refmodes and constructors. Refmodes are a good option when the identifier of an entity is a single primitive value, constructors can be used for multiple keys as well. To learn more, the reference manual contains more information on entity predicates, as well as refmode predicates and constructor predicates.