C3 AI Documentation Home

Functional Patterns

An important feature of the C3 AI Type System is its use of immutability and functional patterns. Immutability plays a significant role in functional programming, where the emphasis is on writing pure functions that avoid side effects and shared mutable state.

Immutability

Immutability refers to the concept that an object cannot be modified after it has been created. This contrasts with mutable objects, which can have their state or data changed after creation.

Creating an new instance of a Type

To make a new instance of an existing custom Type with some fields modified, you have the following options:

  • Create a new instance, and copy the values to the new instance.
  • Use the Obj withField method to return a new instance with a specific field changed.
  • Use a builder.

A simple Type with two fields:

Type
// file name:  Complex.c3typ
type Complex {
  r: !double
  i: !double
}

In the first method, the easiest way to make a new instance is to copy the object into a new variable:

Python
c = 23 # assign a new value to the variable c
d = c3.Complex(r=c.r, i=c.i*2)

Using the withField method is the most convenient when a single field is changed:

Python
c = 23 # assign a new value to the variable c
d = c.withField(c.i * 2)

A ObjBuilder can also be instantiated from an instantiated instance, have the fields set, and then a new instance can be allocated:

Python
c = 23 # assign a new value to the variable c
d = c.toBuilder().i(c.i * 2).build()

Note: The syntax takes advantage of chaining.

Chaining

Chaining refers to the practice of combining multiple operations or methods in a single statement or expression.

In the Type System, chaining can be used to organize the code in such a way that multiple methods can be called sequentially on the same object in a single statement.

A collection field is never null / None, so it can always be used naturally. If it has no elements, no space is used in the database, and it does not appear in the serialized JSON value, but it can be used in-memory.

JavaScript
var equations = Polynomial.fetch({ filter: "solutions.size() > 1" })  // FetchSpec
                          .objs                                       // [Polynomial]
                          .pluck('equation');                         // [any]

In FetchResult#objs, the objs field might be empty (if nothing matched), but is never null / None, so you can perform this natural chaining without having to write if statements. Compare this to the more traditional procedural style of plain JavaScript:

JavaScript
let result = Polynomial.fetch({
    filter: "solutions.size() > 1"
});
let equations = [];
if (result.objs != null) {
    for (let i = 0; i < result.objs.length; i++) {
        equations.push(result.objs[i].equation);
    }
}

Functional programming is preferable when dealing with immutable instances.

Recall the pluck call: .pluck('equation'). Because Collection#pluck cannot have a single Type for any field of any Obj value, it must return an [any]. You can also be more explicit and use a lambda to pull out the field value you want, and create an array of a specific Type:

JavaScript
.mapTo(PrimitiveType.ofStr(), p => p.equation); // [string]

mapTo is a member method, and its input is a collection of a known Type (an array of Polynomials in this case). The return type PrimitiveType#ofStr is the element Type, and the map* variants return the same sort of collection. This line converts a [Polynomial] (array of Polynomial instances) into a [string] (array of primitive strings).

Types are represented as instances of Types and can be used by code. In the C3 Agentic AI Platform, even the built-in behavior is implemented with declared Types. For example, ValueType.c3typ is just a more complex example of the Types demonstrated in this topic.

Was this page helpful?