Predicates and Fields

The heart of an ORM is defining the mapping between the predicates and Python objects. In Clorm this is acheived by sub-classing the Predicate class and specifying fields that map to the ASP predicate parameters.

The Basics

It is easiest to explain this mapping by way of a simple example. Consider the following ground atoms for the predicates address/2 and pets/2. This specifies that the address of the entity dave is "UNSW Sydney" and dave has 1 pet.

address(dave, "UNSW Sydney").
pets(dave, 1).

Note

A note on ASP syntax. All predicates must start with a lower-case letter and consist of only alphanumeric characters (and underscore). ASP supports three basic types of terms (i.e., the parameters of a predicate); a constant, a string, and an integer. Like the predicate names, constants consist of only alphanumeric characters (and underscore) with a starting lower-case character. This is different to a string, which is quoted and can contain arbitrary characters including spaces.

ASP syntax also supports complex terms (also called functions but we will avoid this usage to prevent confusion with Python functions) which we will discuss later. Note, however that ASP does not support real number values.

To provide a mapping that satisfies the above predicate we need to sub-class the Predicate class and use the ConstantStr type specifier as well as the standard int and str to define the individual terms.

from clorm import Predicate, ConstantStr, IntegerField, field

class Address(Predicate):
   entity: ConstantStr
   details: str

class Pet(Predicate):
   entity: ConstantStr
   num: int = field(IntegerField, default=0)

The type annotations specify how the fields are to be translated to Clingo. The entity fields map Python strings to ASP constants, while the Pet’s details field maps Python strings to ASP strings. In constrast the Pet’s num field overrides its default int mapping by using the field() function to explicitly provide the field mapping as well as a default value. So, with the above class definitions we can instantiate some objects:

fact1 = Address(entity="bob", details="Sydney uni")
fact2 = Pet(entity="bob")
fact3 = Pet(entity="bill", num=2)

These object correspond to the following ASP ground atoms (i.e., facts):

address(bob,"Sydney uni").
pet(bob,0).
pet(bill,2).

There are some things to note here:

  • Predicate names: ASP uses standard logic-programming syntax, which requires that the names of all predicate/complex-terms must begin with a lower-case letter and can contain only alphanumeric characters or underscore. Unless overriden, Clorm will automatically generate a predicate name for a Predicate sub-class by transforming the class name based on some simple rules:

    • If the first letter is a lower-case character then this is a valid predicate name so the name is left unchanged (e.g., myPredicate => myPredicate).

    • Otherwise, replace any sequence of upper-case only characters that occur at the beginning of the string or immediately after an underscore with lower-case equivalents. The sequence of upper-case characters can include non-alphabetic characters (eg., numbers) and this will still be treated as a single sequence of upper-case characters.

    • The above criteria covers a number of common naming conventions:

      • Snake-case: My_Predicate => my_predicate, MY_Predicate => my_predicate, My_Predicate_1A => my_predicate_1a,

      • Camel-case: MyPredicate => myPredicate, MyPredicate1A => myPredicate1A.

      • Acronym: TCP1 => tcp1.

  • Field order: the order of declared term defintions in the predicate class is important.

  • Field names: besides the Python keywords, Clorm also disallows the following reserved words: raw, meta, clone, Field as these are used as properties or functions of a Predicate object.

  • Constant vs string: In the above example "bob" and "Sydney uni" are both Python strings but because of the entity field is declared as a ConstantStr (or the explicit ConstantField specifier) this ensures that the Python string "bob" is treated as an ASP constant. Note, currently it is the users’ responsibility to ensure that the Python string passed to a constant term satisfies the syntactic restriction.

  • The use of a default value: all term types support the specification of a default value.

  • If the specified default is a function then this function will be called (with no arguments) when the predicate/complex-term object is instantiated. This can be used to generated unique ids or a date/time stamp.

Overriding the Predicate Name

As mentioned above, by default the predicate name is calculated from the corresponding class name by transforming the class name to match a number of common naming conventions. However, it is also possible to override the default predicate name with an explicit name.

There are many reasons why you might not want to use the default predicate name mapping. For example, the Python class name that would produce the desired predicate name may already be taken. Alternatively, you might want to distinguish between predicates with the same name but different arities. Note: having predicates with the same name and different arities is a legitimate and common practice with ASP programming.

class Address2(Predicate, name="address"):
   entity: ConstantStr
   details: str

 class Address3(Predicate, name="address"):
   entity: ConstantStr
   details: str
   country: str

Instantiating these classes:

shortaddress = Address2(entity="dave", details="UNSW Sydney")
longaddress = Address3(entity="dave", details="UNSW Sydney", country="AUSTRALIA")

will produce the following matching ASP facts:

address(dave, "UNSW Sydney").
address(dave, "UNSW Sydney", "AUSTRALIA").

Nullary Predicates

A nullary predicate is a predicate with no parameters and is also a legitimate and reasonable thing to see in an ASP program. Defining a corresponding Python class is straightforward:

class ANullary(Predicate):
    pass

fact = ANullary()

The important thing to note here is that every instantiation of ANullary will correspond to the same ASP fact:

aNullary.

Complex Terms

So far we have shown how to create Python definitions that match predicates with simple terms. However, in ASP it is common to also use complex terms within a predicate, such as:

booking("2018-12-31", location("Sydney", "Australia")).

The Clorm Predicate class definition is able to support the flexiblity required to deal with complex terms.

from clorm import Predicate

class Location(Predicate):
   city: str
   country: str

class Booking(Predicate):
    date: str
    location: Location

Note

There is also a ComplexTerm class which is an alias for the Predicate class. For personal stylistic reasons you may prefer to use this alias to define classes that will only be used as complex terms. However there are cases where this separation breaks down. For example when dealing with the reification of facts there is nothing to be gained by providing two definitions for the predicate and complex term versions of the same non logical term:

p(q(1)).
q(1) :- p(q(1)).

In this example q/1 is both a complex term and predicate and when providing the Python Clorm mapping it is simpler not to separate the two versions:

class Q(Predicate):
   a: int

class P(Predicate):
   a: Q

The predicate class containing complex terms can be instantiated in the obvious way:

bk=Booking(date="2018-12-31", location=Location(city="Sydney",country="Australia"))

As with the primitive terms it is possible to override the translation of complex terms, for example to provide defaults, by using the field() function. While the first parameter of the function must be a sub-class of BaseField, fortunately, every predicate sub-class has a corresponding, internally generated, BaseField sub-class which can be accessed though the Field property of that predicate class. So for example we can modify the Booking class definition to provide a default location.

class Booking(Predicate):
    date: str
    location: Location = field(Location.Field, default=Location("Potsdam", "Germany")

bk2=Booking(date="2019-12-14")

This second booking instance will correspond to the fact:

booking("2019-12-14", location("Potsdam", "Germany")).

Negative Facts

ASP follows standard logic programming syntax and treats the not keyword as default negation (also negation as failure). Using default negation is important to ASP programming as it can lead to more readable and compact modelling of a problem.

However, there may be times when having an explicit notion of negation is also useful, and ASP/Clingo does have support for classical negation; indicated syntactically using the - symbol:

{ a(1..2); b(1..2) }.
-b(N) :- a(N).
-a(N) :- b(N).

The above program chooses amongst the a/1 and b/1 predicates, then for every positive a/1 fact, the corresponding b/1 fact is negated and vice-versa. This will generate nine stable models. For example, if a(2) and b(1) are chosen, then the corresponding negative literals will be -b(2) and -a(1) respectively.

Note: Clingo supports negated literals as well as terms. However, tuples cannot be negated.

f(-g(a)).   % This is valid
f(-(a,b)).  % Error!!!

Clorm supports negation for any fact or term that can be negated by Clingo. Specifying a negative literal simply involves setting sign=False when instantiating the Predicate (or ComplexTerm). Note: unlike the field parameters, the sign parameter must be specified as a named parameter and cannot be specified using positional arguments.

class P(Predicate):
    a: int

neg_p1 = P(a=1,sign=False)
neg_p1_alt = P(1,sign=False)
assert neg_p1 == neg_p1_alt

Once instantiated, checking whether a fact (or a complex term) is negated can be determined using the sign attribute of Predicate instance.

assert neg_p1.sign == False

Finally, for finer control of the unification process, a Predicate/ComplexTerm can be specified to only unify with either positive or negative facts/terms by setting a sign meta attribute declaration.

class P_pos(Predicate, name="p", sign=True):
    a: int

class P_neg(Predicate, name="p", sign=False):
    a: int

% Instatiating facts
pos_p = P_pos(1)                     % Ok
neg_p_fail = P_pos(1,sign=False)     % throws a ValueError

neg_p = P_neg(1)                     % Ok
pos_p_fail = P_neg(1,sign=False)     % throws a ValueError

% Unifying against raw Clingo positive and negative facts
raws = [Function("p",Number(1)), Function("p",Number(1),positive=False)]
fb = unify([P_pos,P_neg], raw)
assert pos_p in fb
assert neg_p in fb

Field Definitions

Clorm provides a number of standard definitions that specify the mapping between Clingo’s internal representation (some form of Clingo.Symbol) to more natural Python representations. ASP has three simple terms: integer, string, and constant, and Clorm provides three standard definition classes to provide a mapping to these fields: IntegerField, StringField, and ConstantField.

Clorm also provides a SimpleField class that can match to any simple term. This is useful when the parameter of a defined predicate can contain arbitrary simple term types. Clorm takes care of converting the ASP string, constant or integer to a Python string or integer object. Note that both ASP strings and constants are both converted to Python string objects.

In order to convert from a Python string object to an ASP string or constant, SimpleField uses a regular expression to determine if the string matches the pattern of a constant and treats it accordingly. For this reason SimpleField should be used with care in order to ensure expected behaviour, and using the distinct field types is often preferable.

Sub-classing Field Definitions

All field classes inherit from a base class BaseField and it’s possible to define arbitrary data conversions by sub-classing BaseField. Clorm provides the standard sub-classes StringField, ConstantField, and IntegerField. Clorm also automatically generates an appropriate sub-class for every Predicate definition for use in a complex term.

However, it is sometimes also useful to explicitly sub-class the BaseField class, or sub-class one of its sub-classes. By sub-classing a sub-class it is possible to form a data conversion chain. To understand why this is useful we consider an example of specifying a date field.

Consider the example of an application that needs a date term for an event tracking application. From the Python code perspective it would be natural to use Python datetime.date objects. However, it then becomes a question of how to encode these Python date objects in ASP (noting that ASP only has three simple term types).

A useful encoding would be to encode a date as a string in YYYYMMDD format (or YYYY-MM-DD for greater readability). Dates encoded in this format satisfy some useful properties such as the comparison operators will produce the expected results (e.g., "20180101" < "20180204"). A string is also preferable to using a similiarly encoded integer value. For example, encoding the date in the same way as an integer would allow incrementing or subtracting a date encoded number, which could lead to unwanted values (e.g., 20180131 + 1 = 20180132 does not correspond to a valid date).

So, adopting a date encoded string we can consider a date based fact for the booking application that simply encodes that there is a New Year’s eve party on the 31st December 2018.

booking("2018-12-31", "NYE party").

Using Clorm this fact can be captured by the following Python Predicate sub-class definition:

from clorm import *

class Booking(Predicate):
   date: str
   description: str

However, since we encoded the date as simply a str (which internally maps to StringField) it is now up to the user of the Booking class to perform the necessary translations to and from a Python datetime.date objects when necessary. For example:

import datetime
nye = datetime.date(2018, 12, 31)
nyeparty = Booking(date=int(nye.strftime("%Y-%m-%d")), description="NYE Party")

Here the Python nyeparty variable corresponds to the encoded ASP event, with the date term capturing the string encoding of the date. In the opposite direction to extract the date it is necessary to turn the date encoded string into an actual datetime.date object:

nyedate = datetime.datetime.strptime(str(nyepart.date), "%Y-%m-%d")

The problem with the above code is that the process of creating and using the date in the Booking object is cumbersome and error-prone. You have to remember to make the correct translation both in creating and reading the date. Furthermore the places in the code where these translations are made may be far apart, leading to potential problems when code needs to be refactored.

The solution to this problem is to create a sub-class of BaseField that performs the appropriate data conversion. However, sub-classing Basefield directly requires dealing with raw Clingo Symbol objects. A better alternative is to sub-class the StringField class so you only need to deal with the string to date conversion.

import datetime
from clorm import *

class DateField(StringField):
    pytocl = lambda dt: dt.strftime("%Y-%m-%d")
    cltopy = lambda s: datetime.datetime.strptime(s,"%Y-%m-%d").date()

class Booking(Predicate):
    date: datetime.date = field(DateField)
    description: StringField

The pytocl definition specifies the conversion that takes place in the direction of converting Python data to Clingo data, and cltopy handles the opposite direction. Because the DateField inherits from StringField therefore the pytocl function must output a Python string object. In the opposite direction, cltopy must be passed a Python string object and performs the desired conversion, in this case producing a datetime.date object.

With the newly defined DateField the conversion functions are all captured within the one class definition and interacting with the objects can be done in a more natural manner.

nye = datetime.date(2018,12,31)
nyeparty = Booking(date=nye, description="NYE Party")

print("Event {}: date {} type {}".format(nyeparty, nyeparty.date, type(nyeparty.date)))

will print the expected output:

Event booking(20181231,"NYE Party"): date "2018-12-31" type <class 'datetime.date'>

Note

The pytocl and cltopy functions can potentially be passed bad input. For example, when converting a clingo String symbol to a date object the passed string may not correspond to an actual date. In such cases these functions can legitimately throw either a TypeError or a ValueError exception. Internally, Clorm’s framework will catch these two types of exceptions and will treat them as failures to unify when trying to unify clingo symbols to facts. Any other exception is passed through as a genuine error. This should be kept in mind if you are writing your own field class.

Restricted Sub-class of a Field Definition

Another reason to sub-class a field definition is to restrict the set of values that the field can hold. For example you could have an application where an argument of a predicate is restricted to a specific set of constants, such as the days of the week.

cooking(monday, "Jane"). cooking(tuesday, "Bill"). cooking(wednesday, "Bob").
cooking(thursday, "Anne"). cooking(friday, "Bill").
cooking(saturday, "Jane"). cooking(sunday, "Bob").

When defining a predicate corresponding to cooking/2 it is possible to simply use a ConstantField field for the days.

class Cooking1(Predicate):
   dow = ConstantField
   person = StringField
   class Meta: name = "cooking"

However, this would potentiallly allow for creating erroneous instances that don’t correspond to actual days of the week (for example, with a spelling mistake):

ck = Cooking1(dow="mnday",person="Bob")

In order to avoid these errors it is necessary to subclass the ConstantField in order to restrict the set of values to the desired set. Clorm provides a helper function refine_field() for this use-case. It dynamically defines a new class that restricts the values of an existing field class.

DowField = refine_field(ConstantField,
   ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"])

class Cooking2(Predicate, name="cooking"):
   dow: ConstantStr = field(DowField)
   person:str

ok=Cooking2(dow="monday",person="Bob")

try:
   bad = Cooking2(dow="mnday",person="Bob")  # raises a TypeError exception
except TypeError:
   print("Caught exception")

Note

The refine_field() function can also be called with only two arguments, rather than three, by ignoring the name for the generated class. In this case an anonymously generated name will be used.

As well as explictly specifying the set of refinement values, refine_field() also provides a more general approach where a function/functor/lambda can be provided. This function must take a single input and return True if that value is valid for the field. For example, to define a field that accepts only positive integers:

PosIntField = refine_field(NumberField, lambda x : x >= 0)

An alternative to using refine_field() to restrict the allowable values is to an explicitly specified set is to use define_enum_field(). This function allows Clorm to be used with standard Python Enum classes. So, the day-of-week example could be rewritten to use an Enum class:

import enum

class DOW(ConstantStr, enum.Enum):
    SUNDAY="sunday"
    MONDAY="monday"
    TUESDAY="tuesday"
    WEDNESDAY="wednesday"
    THURSDAY="thursday"
    FRIDAY="friday"
    SATURDAY="saturday"

class Cooking3(Predicate, name="cooking"):
    dow: DOW
    person: str

ok = Cooking3(dow=DOW.MONDAY,person="Bob")

One useful advantage of using an enumeration is Clorm has built in handling to allow it to be specified as a type annotation. This means that you do not have to explicitly call the define_enum_field() function to generate the appropriate field definition.

Finally, it should be highlighted that this mechanism for defining a field restriction works not just for validating the inputs into an ASP program. It can also be used to filter the outputs of the ASP solver as the invalid field values will not unify with the predicate.

For example, in the above program you can separate the cooks on the weekend from the weekday cooks.

WeekendField = refine_field(ConstantField, ["sunday","saturday"])
WeekdayField = refine_field(ConstantField, ["monday","tuesday","wednesday","thursday","friday"])

class WeekendCooking(Predicate, name="cooking"):
   dow: str = field(WeekendField)
   person: str

class WeekdayCooking(Predicate, name="cooking"):
   dow: str = field(WeekdayField)
   person: str

Using Positional Arguments

So far we have shown how to create Clorm predicate and complex term instances using keyword arguments that match their defined field names, as well as accessing the arguments via the fields as named properties. For example:

from clorm import *

class Contact(Predicate):
    cid: int
    name: str

c1 = Contact(cid=1, name="Bob")

assert c1.cid == 1
assert c1.name == "Bob"

However, Clorm also supports creating and accessing the field data using positional arguments:

c2 = Contact(2,"Bill")

assert c2[0] == 2
assert c2[1] == "Bill"

While Clorm does support the use of positional arguments for predicates, nevertheless it should be used sparingly because it can lead to brittle code that can be hard to debug, and can also be more difficult to refactor as the ASP program changes. However, there are genuine use-cases where it can be convenient to use positional arguments. In particular when defining very simple tuples, where the position of arguments is unlikely to change as the ASP program changes. We discuss Clorm’s support for these cases in the following section.

Working with Tuples

Tuples are a special case of complex terms that often appear in ASP programs. For example:

booking("2018-12-31", ("Sydney", "Australia)).

For Clorm tuples are simply a Predicate sub-class where the name of the corresponding predicate is empty. While this can be set using an is_tuple property of the complex term’s class, Clorm also provides specialised support using the more intuitive syntax of a Python tuple type annotations. For example, a predicate definition that unifies with the above fact can be defined simply (using the DateField defined earlier):

class Booking(Predicate):
    date: datetime.date = field(DateField)
    location: tuple[str, str]

Note

For Python versions earlier than 3.9 you need to specify the tuple type using the Tuple identifier from the typing module:

 from typing import Tuple

class Booking(Predicate):
    date: datetime.date = field(DateField)
    location: Tuple[str, str]

Here the location field is defined as a pair of strings, without having to explictly define a separate ComplexTerm sub-class that corresponds to this pair. To instantiate the Booking class a Python tuple can also be used for the values of location field. For example, the following creates a Boooking instance corresponding to the booking/2 fact above:

bk = Booking(date=datetime.date(2018,12,31), location=("Sydney","Australia"))

While it is unnecessary to define a seperate Predicate sub-class corresponding to the tuple, internally this is in fact exactly what Clorm does. Clorm will transform the above definition into something similar to the following:

class SomeAnonymousName(Predicate, name=""):
   city: str
   country: str

class Booking(Predicate):
    date: datetime.date = field(DateField)
    location: tuple[str, str] = field(SomeAnonymousName.Field)

Here the Predicate has an empty name, so it will be treated as a tuple rather than a complex term with a function name.

One important difference between the implicitly defined and explicitly defined versions of a tuple is that the explicit version allows for field names to be given, while the implicit version will have automatically generated names. However, for simple implicitly defined tuples it would be more common to use positional arguments anyway, so in many cases it can be the preferred alternative. For example:

bk = Booking(date=datetime.date(2018,12,31), location=("Sydney","Australia"))

assert bk.location[0] == "Sydney"

Note

As mentioned previously, using positional arguments is something that should be used sparingly as it can lead to brittle code that is more difficult to refactor. It should mainly be used for cases where the ordering of the fields in the tuple is unlikely to change when the ASP program is refactored.

Debugging Auxiliary Predicates

When integrating an ASP program into a Python based application there will be a set of predicates that are important for inputting a problem instance and outputting a solution. Clorm is intended to provide a clean way of interacting with these predicates.

However, there will typically be other auxiliary predicates that are used as part of the problem formalisation. While they may not be important from the Python application point of view they do become important during the process of developing and debugging the ASP program. During this process it can be cumbersome to build a detailed Clorm predicate definition for each one of these, especially when all you need to do is print the predicate instances to the screen, possibly sorted in some order.

Clorm solves this issue by providing a factory helper function simple_predicate() that returns a Predicate sub-class that will map to any predicate instance with that name and arity.

For example this function could be used for the above booking example if we wanted to extract the booking/2 facts from the model but didn’t care about mapping the data types for the individual parameters. For example to match the ASP fact:

booking("2018-12-31", ("Sydney", "Australia)).

instead of the explicit Booking definition above we could use the simple_predicate() function:

from clorm.clingo import Symbol, Function, String
from clorm import _simple_predicate

Booking_alt = simple_predicate("booking",2)
bk_alt = Booking_alt(String("2018-12-31"), Function("",[String("Sydney"),String("Australia")]))

Note, in this case in order to create these objects within Python it is necessary to use the Clingo functions to explictly create clingo.Symbol objects.

Dealing with Raw Clingo Symbols

As well as supporting simple and complex terms it is sometimes useful to deal with the raw clingo.Symbol objects created through the underlying Clingo Python API.

Raw Clingo Symbols

The Clingo API uses clingo.Symbol objects for dealing with facts; and there are a number of functions for creating the appropriate type of symbol objects (i.e., clingo.Function(), clingo.Number(), clingo.String()).

In essence the Clorm Predicate class simply provides a more convenient and intuitive way of constructing and dealing with these clingo.Symbol objects. In fact the underlying symbols can be accessed using the raw property of a Predicate instance.

from clorm import *    # Predicate, ConstantField, StringField
from clingo import *   # Function, String

class Address(Predicate):
   entity: ConstantStr
   details: str

address = Address(entity="dave", details="UNSW Sydney")

raw_address = Function("address", [Function("dave",[]), String("UNSW Sydney")])

assert address.raw == raw_address

Note

To construct clorm objects from raw clingo symbols involves unifying the clingo symbol with the Predicate or ComplexTerm sub-class. This typically happens when you have a list of symbols corresponding to a clingo model and you want to turn them into a set of clorm facts. See Unification, Integration with the Solver, and unify() for details about unification.

Integrating Clingo Symbols into a Predicate Definition

There are some cases when it might be convenient to combine the simplicity and the structure of the Clorm predicate interface with the flexibility of the underlying Clingo symbol API. For this case it is possible to use the RawField class.

For example when modeling dynamic domains it is often useful to provide a predicate that defines what fluents hold (i.e., are true) at a given time point, but to allow the fluents themselves to have an arbitrary form.

time(1..5).

holds(X,T+1) :- fluent(X), not holds(X,T).

fluent(light(on)).
fluent(robotlocation(roby, kitchen)).

holds(light(on), 0).
holds(robotlocation(roby,kitchen), 0).

In this example instances of the holds/2 predicate can have two distinctly different signatures for the first term (i.e., light/1 and robotlocation/2). While the definition of the fluent is important at the ASP level, however, at the Python level we may not be interested in the structure of the fluent, only whether it holds or not. In such a case we can use a RawField to define the raw mapping from the fluent term to a Python object.

from clorm import Raw, Predicate

class Holds(Predicate):
   fluent: Raw
   time: int

RawField provides no data translation between ASP and Python and therefore has the useful property that it will unify with any clingo.Symbol object; in particular in this case it can be used to capture both the light/1 and robotlocation/2 complex terms.

When translating from Python to clingo, RawField expects objects of the type Raw, and returns objects of this type when translating from clingo to Python. Raw is simply a thin wrapper around the underlying clingo.Symbol.

For example, to create a create a Python fact that specifies that the light is on at time 0:

from clingo import Function
from clorm import Raw

sym_lighton = Function("light", [Function("on",[])])
lighton1 = Holds(fluent=Raw(sym_lighton), time=0)

Combining Field Definitions

The above example is useful for cases where you don’t care about accessing the details of individual fluents and therefore it makes sense to simply treat them as a RawField complex term. However, the question naturally arises what to do if you do want more fine-grained access to these fluents.

There are a few possible solutions to this problem, but one obvious answer is to use a field that combines together multiple fields. Such a combined field could be specified manually by explicitly defining a BaseField sub-class. However, to simplify this process the combine_fields() factory function has been provided that will return such a combined sub-class. In fact Clorm uses standard Python union type annotation to implicitly generate such a mapping.

With reference to the ASP code of the previous example we could add the following Python integration:

from clorm import Predicate, ComplexTerm, IntegerField, ConstantField, combine_fields

class Light(Predicate):
   status: ConstantStr

class RobotLocation(Predicate, name="robotlocation"):
   robot: ConstantStr
   location: ConstantStr

class Holds(Predicate):
   fluent: Light | RobotLocation
   time: int

Note

For Python versions earlier than 3.11 you need to specify the union type using the Union identifier from the typing module:

from typing import Tuple

class Holds(Predicate):
   fluent: Union[Light, RobotLocation]
   time: int

When used explicitly, the combine_fields() function takes two arguments; the first is an optional field name argument and the second is a list of the sub-fields to combine. Note: when trying to unify a value with a combined field the raw symbol values will be unified with the underlying field definitions in the order that they are listed in the call to combine_fields(). This means that care needs to be taken if the raw symbol values could unify with multiple sub-fields; it will only unify with the first successful sub-field. In the above example this is not a problem as the two fluent field definitions do not overlap.

Dealing with Nested Lists

ASP does not have an explicit representation for lists. However a common convention for encoding lists is using a nesting of head-tail pairs; where the head of the pair is the element of the list and the tail is the remainder of the list, being another pair or an empty tuple to indicate the end of the list.

For example encoding a list of “nodes” [1,2,c] for some predicate p, might take the form:

p(nodes,(1,(2,(c,())))).

While, such an encoding can be problematic and can lead to a grounding blowout, nevertheless when used with care can be very useful.

Unfortunately, getting facts containing these sorts of nested lists into and out of Clingo can be very cumbersome. To help support this type of encoding Clorm provides the define_nested_list_field() function. This factory function takes an element field class, as well as an optional new class name, and returns a newly created BaseField sub-class that can be used to convert to and from a list of elements of that field class. Clorm provides implicit support for this helper function with some extra type identifiers.

from clorm import Predicate, ConstantStr, HeadList

class P(Predicate):
   param: ConstantStr
   alist: HeadList[int]

p = P("nodes",[1,2,3])
assert str(p) == "p(nodes,(1,(2,(3,()))))"

Old Syntax

The preferred syntax for specifying predicates has changed with Clorm 1.5. The new syntax looks very similar to standard Python dataclasses or a modern Python library such as Pydantic. This new syntax integrates better with modern Python programming practices, for example using linters and type checkers.

The old syntax does not use Python type annotations and instead required the user to explicitly a BaseField sub-class for each term. It also required the use of a Meta sub-class to provide predicate meta-data, for example, to override the name of the predicate.

from clorm import Predicate, StringField, IntegerField

class Location(Predicate):
   city = StringField
   country = StringField

   class Meta:
      name = "mylocation"

class Booking(Predicate):
    date = StringField
    location = Location.Field

While the old syntax still works it should only be used as a fallback if it is not possible to specify some requirement using the new syntax. The old syntax will likely be deprecated at some point and eventually removed completely.