Understanding Facts¶
Want more background information on Vulcan's Facts?
Facts are the basis of logic within Vulcan. Facts represent the domain model for which Vulcan performs its reasoning via Rules. Collectively, they serve as input, output, and the state of intermediate reasoning steps. A collection of Fact definitions may be referred to as a "knowledge schema" or "Facts schema."
A simple Fact definition and instantiation:
| Python | |
|---|---|
Working Memory¶
The Vulcan Working Memory is a collection of Fact instances that represent the state of your domain model. For example, the working memory for a grocery story may contain Facts representing inventory, item types and prices, equipment temperatures, store operating hours, and so on. The working memory simultaneously serves as the input, output, and intermediate state of the rules engine.
In simple terms, the Working Memory can be thought of as a Python dictionary. However, a simple dictionary of key-value strings is insufficient to express non-trivial reasoning tasks. Therefore, Vulcan's Working Memory operates only on Fact objects, enabling complex expression of semantics and reasoning within Rules.
Fact Constraints¶
Python Typing
If you are unfamiliar with Python 3 typing, check out the MyPy Python 3 Typing Cheat Sheet to understand the basic concepts and syntax.
Vulcan's syntax is designed to be simple, yet flexible enough to express complex reasoning tasks. By leveraging Python's typing and metaprogramming features, Vulcan encourages a declarative style of programming that enhances the developer experience when used with modern IDE static analysis and autocompletion. As a result, boilerplate code and the possibility of errors are minimized.
To ensure a consistent and error-resistant interface for Vulcan rules, Vulcan imposes some requirements on how Facts may be defined and used.
Facts Must Extend the Fact Class¶
Vulcan Facts must extend the base class vulcan_core.Fact. The Fact base class provides development conveniences, such as:
- Automatically apply Python dataclass functionality.
- Ensures that all Facts are immutable.
- Enforces that Facts may only be instantiated with keyword arguments.
- Adds operators for working with immutability, such as the union operator:
| - Provide dictionary-like access to the Fact's attributes.
Omitting the Fact base class will cause Facts to fail static and runtime checks:
Incorrect: Using a non-Fact class
| Python | |
|---|---|
- Modern IDEs with type checking will eagerly display an error on this line.
| Error | |
|---|---|
Type Hints are Required¶
Correct
- All Fact fields must be declared with type information such as
int,str,float, etc.
Vulcan requires type information for all fields defined in a Fact. This helps to catch errors early in the development process and improve the overall reliability of the rules.
Facts missing type hints will result in static and runtime errors:
Incorrect: Missing type hint
- Modern IDEs with linting will eagerly display an error on this line.
| Error | |
|---|---|
Keyword Instantiation is Required¶
Correct
- Facts may only be instantiated using keyword arguments.
To facilitate readability and reduce the risk of errors, Vulcan enforces that all Facts must be instantiated with keyword arguments. This means that when creating an instance of a Fact, you must specify the names of the fields along with their values.
Positional arguments are not allowed when instantiating Facts and will result in static and runtime errors:
Incorrect: Instantiation using positional arguments
- Modern IDEs with linting will eagerly display an error on this line.
| Error | |
|---|---|
Facts are Immutable¶
Vulcan requires all Facts to be immutable. This means that once a Fact is created, its attributes cannot be modified. This immutability is crucial for ensuring the integrity of reasoning and preventing unintended side effects.
To simplify Fact manipulation with immutability, Vulcan provides a union operator similar to the dictionary union operator: |. Vulcan also allows partial creation of Facts using the functools.partial function. We will explore both of these features more in the Working With Facts section below.
Attempting to modify a Fact instance fields directly will result in static and runtime errors:
Incorrect: Modifying a Fact instance
| Python | |
|---|---|
- Modern IDEs with linting will eagerly display an error on this line.
| Error | |
|---|---|
Working With Facts¶
Facts are immutable to ensure the integrity of reasoning and prevent unintended side effects from actors external to the reasoning engine. Generally speaking, immutability can be difficult to work with, especially if applying design patterns that rely on mutability. In most use-cases Vulcan's Fact immutability should be of little inconvenience for developers - as long as Vulcan's interface contracts and best practices are followed.
Vulcan offers features to help make working with facts relatively easy. The first is that Vulcan supports the concept of a partial Facts using Python's functools.partial function. This allows partial instantiation and limited mutability, prior to the final instantiation of a Fact.
- As long as a Fact is a
partialFact, any defined field may be modified. - Once the partial Fact has been fully instantiated, it may no longer be modified.
| Text Output | |
|---|---|
Partial Facts are Mutable
While fully-instantiated Facts are immutable, a partial Fact's fields can be modified prior to instantiation. This allows a degree of flexibility before Facts are added to Vulcan's working memory.
Partial facts are most often found in a Rule's Action. For example a developer may intend to update only a portion of a Fact, without wanting to replace or repeat the values of the entire Fact.
Default Fact Values¶
You may have noticed in prior examples, that Fact fields may be defined with default values:
| Python | |
|---|---|
- A default value of
10 - Fields without defaults will require a value at instantiation, or resolvable at runtime if the fact is a
partialFact. - A field with an optional and default value of
None
Default values are optional. But if defaults are not defined, values will be required when the Fact is instantiated. This includes when a value is missing from a partial Fact. Static and runtime errors will be raised if default values are not provided at instantiation.
Avoid Excessively Defining Defaults
Be careful to not add defaults simply to avoid errors. Depending on what the Fact represents, and when it will be used (input, output, or intermediate), missing value errors can be helpful to catch invalid logic states and assignments. This is especially true with intermediate and output Facts.
Fact Union Operator¶
Information represented by Facts is expected to change over time, but Fact immutability means that existing facts must be replaced by new Facts in order to update the overall state (working memory) of the rules engine. Vulcan Rules abstract the concerns of immutability away from developers, but there are times when a developer may want to create altered Facts outside of a Rule definition.
To make derivative Fact creation easier, Vulcan provides a union operator to combine a Fact instance with a partial Fact.
| Python | |
|---|---|
| Text Output | |
|---|---|
Beware: Using Union Operator with Default Values
If given a fully-instantiated Fact on the right-side of the expression, the union operator will return a Fact with all values "merged" from the right operand. While logically this is expected given the function of the operator, doing so with Facts that provide default values may result in unexpected results in practice.
Accidentally creating a new Fact with defaults, instead of specifying a partial Fact, will result in the default Fact values overriding existing values:
| Python | |
|---|---|
- Using a fully-instantiated Fact on the right side of the union operator will "replace" any matching fields in the left, including the default values defined for the Fact.
| Text Output | |
|---|---|
Dictionary Key Access¶
Unlike typical Python dataclasses, Vulcan Facts also support dictionary-like access to their attributes. This allows greater interoperability with other Python libraries and tools, such as JSON serialization.
| Python | |
|---|---|
Using Facts With Conditions and Actions¶
Facts are often used within Vulcan's condition and action Rule expressions. Vulcan uses a modified form of Python's lambda expression, to both allow linters and type checkers to validate the expression, while providing developers with a declarative syntax.
Conditions are used to determine if a Rule should be executed. Below is an example of using a Fact within a standalone condition:
| Python | |
|---|---|
- Vulcan lambdas will only accept a
<classname>.<fieldname>syntax for accessing Fact fields.
During Rule evaluation, Conditions and Actions are provided values from the Facts residing in Vulcan's working memory. In order for Vulcan to manage dynamic rule evaluation, access to specific Fact instance is not allowed in lambda expressions. If attempted, Conditions and Actions will raise a runtime error:
Incorrect: Using fact instances within a condition
| Error | |
|---|---|
Facts may also be used within Actions. When a Rule's Condition criteria is met, it will apply the Action to the working memory. Actions may apply Facts, partial Facts, and use lambda expressions to reference other Facts within the working memory.
Below are some ways in which Facts may be used within Actions:
| Python | |
|---|---|
- Replaces or creates a new Fact ignoring any existing values.
- Replaces an existing Fact by merging the the specified values with the previous Fact. If the Fact is not present in the working memory, default values will be used for any missing fields.
- Performs calculations prior to replacing or creating a new Fact.