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.
Reference modes
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
and 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 cost
and price
predicates:
//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 <entity>_<attribute>
:
//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 icecream
instance.
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 store
was value
” in a very natural way:
//lang:logiql
sales[week, ic, store] = val -> week(week), icecream(ic), store(store), int(val).
Constructors
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.
Subtypes
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 sellable
entity:
//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 sellable
ic:
//lang:logiql
icecream(ic) -> sellable(ic).
In this case, however, we do need to also explicitly state that icecream
is an entity itself:
//lang:logiql
lang:entity(`icecream).
In the future we can define other sellable goods, like candy bars:
//lang:logiql
candybar(cb) -> sellable(cb).
lang:entity(`candybar).
Conclusion
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.