Type Inheritance
The C3 Agentic AI Platform supports Type reuse. Type reuse involves leveraging the properties and methods of an existing Type to avoid writing redundant code, and create new Types.
Basic concept
Conceptually you can think of the syntax for reusing a Type like this:
- How the fields and methods of an existing Type are to be reused, and
- Where the data for the new Type should be stored.
The keywords entity, extendable, extends, mixes, remix apply to reusing a Type's fields and methods.
At a high level, the basic syntax to define C3 AI Types is as follows:
[remix] [extendable] [entity] type TypeName [extends SuperTypeOrAnotherType] [mixes SuperTypeOrAnotherType] {
/* comments */
[field declaration]
[method declaration]
}Note: Everything in the code snippet above within square brackets [] is optional.
Create an entity Type
The entity keyword provides the functionality for manipulating and storing the Type's data in one of the C3 AI Platform's databases. An entity Type persist the data in both relational and key-value data stores. Entity Types mix Persistable.
If the entity is stored in the relational database, then this would be a relational database table. For entity Types stored in Cassandra, the column family is logically equivalent.
You define an entity Type using the entity keyword.
entity type MyEntityType {
}An entity Type can also contain references to one or more Types that are not entity Types:
entity type MyEntityType {
referenceType: MyReferenceType
}Persisting reference Types allows more complex objects to be persisted in the database table that is created for MyEntityType, storing the field values of SomeType as columns in the MyEntityType database.
You can read more in depth about Entity Types in the Create an Entity Data Model.
How to extend a Type
Extending a Type refers to creating a new Type that inherits the properties and methods of an existing Type, also known as the parent Type or supertype. The new Type is known as the subtype.
The benefit of this is that it allows for code reuse. By creating a new Type that inherits from an existing Type, you can reuse the functionality of the parent Type and add or override that functionality as needed in the subtype.
The extended Type and original Type must include the entity keyword and share the same database table. To distinguish data from the original and extended C3 AI Types, the C3 Agentic AI Platform adds a type key field to the database table. The keyword type key is an additional field that differentiates which rows belong to which Type.
Make a Type extendable
Including the keyword extendable in the Type's declaration indicates the Type can be extended. That is, the fields and methods of the Type being extended can be reused by the subtype.
Only C3 AI Types marked with the extendable keyword can be extended.
The keyword schema name precedes the database table name. The object instances of this Type are stored in the rows of the table. The use of schema name is optional for non-external Types.
The code snippet below creates a WeatherForecast Type that can be extended by another Type (covered in the next section).
extendable entity type WeatherForecast schema name "WTHR" type key "PARENT" {
// Method for aggregating the weather data for a specific time range (start and end date)
//
// Input must be two strings, each string in the format "YYYY-MM-DD" format
precipitation: function(startData: string, endDate: string): double js-server
}The object instances of the Type WeatherForecast are stored in the same table WTHR (schema name for the Type WeatherForecast). However, the Type key WTHR can be used to mark the WeatherForecast object rows to distinguish these WeatherForecast object instances from the WeatherForecast object instances in the table.
Extend a Type using extends
You use the extends keyword in a Type declaration to indicate that the Type you are declaring is a subtype of the Type identified after extends. The extending Type and the extended Type must be entity Types and share the same table.
For example, you can extend the WeatherForecast Type with the creation of two new Types, ShortRangeForecast and LongRangeForecast.
entity type ShortRangeForecast extends WeatherForecast schema name "SHT_RNG_FRCST" {
// ...
}
entity type LongRangeForecast extends WeatherForecast schema name "LNG_RNG_FRCST" {
// ...
}Note: Only single inheritance is possible. That is, you can only extend one Type in a Type definition. For example, if WeatherForecast and ForecastMetrics are extendable Types, then a syntax such as entity type ShortRangeForecast extends WeatherForecast, ForecastMetrics is invalid.
How to mixin other Types
Types can mixin other Types. This is like sub-classing in Java or C++, but unlike Java which is limited to single inheritance, multiple Types can be mixed in. Mixins are often generic, which means they have unbound variables that are defined by Types that mix them in (at any depth). Fields are inherited, and additional fields can be created on the new Type.
Mixin a Type
Use the mixes keyword in a new Type definition to create a mixin Type.
A Type can mixin multiple existing Types. The order of the Types that are being mixed in matters. See the Working with packages section in this document for an example. You can also combine mixes and extends to create complex and useful Types.
For example:
entity type ShortRangeForecast extends WeatherForecast mixes Evaluatable {
}In the above example, the WeatherForecast Type is defined as an extendable Type, allowing the Type ShortRangeForecast to extend WeatherForecast. Although a Type can only extend one other Type, you can inherit more fields and methods by mixing other Types. In the above example, the ShortRangeForecast Type extends the WeatherForecast Type, it also mixes the Evaluatable Type.
Note the following:
Any Type can mixin a non-entity Type.
A non-entity Type cannot mixin an entity Type.
An entity Type can mixin either a non-entity Type or an abstract entity Type.
An entity Type cannot mixin a non-abstract entity Type. The entity Type should
extendthe non-abstract entity Type instead.extends: A Type is limited to a singleextends, and requires the parent Type to be defined asextendable.mixes: Similar to sub-classing in Java or C++, but unlike Java multiple Types may be mixed in.
Non-Objs and Value
Most of the time, the behavior that comes with Obj is desired. However, there are Types which explicitly opt out of inheriting Obj behavior by mixing Value. These Types have no predefined behavior at all, and explicitly mixin only what they need.
Mixing the Type Value requires extra implementation to reach the same level of support and convenience that is inherited with Obj, therefore, built-in behavior comes in small pieces (known as Types with just a method or two).
Every object that exists in a C3 AI application is stored and declared as a Type, except certain primitives and lambdas. Primitives such as the numeric Types, string, and boolean have a natural representations in every language, and you can use the native instances for performance and convenience. There is no String Type for example, as an instance of the string Type just uses the language-native string Types.
Other than primitives, every instance is a Value.
The Value Type does not declare any methods and is not an Obj so it cannot have fields.
The type() function is inherited from WithType, so it is not guaranteed to be present on non-Obj values (just as it is not on primitive values).
Below is an example for a Type Dodo that mixes in Value:
type Dodo mixes Value {
speak: function(): string js
}function speak() {
return "I can't; I'm extinct.";
}Dodo cannot be instantiated. It is not an Obj, so there is no make function, and Dodo does not declare anything on the Type that would create an instance. Dodo does not mixin Serializable (or StringSerializable), therefore it cannot be serialized for remote procedure calls. Dodo is not useful without more implementation.
How to remix a Type
In addition to mixes (multiple inheritance), a concept called remixing is supported. A remix is a package-specific change to an existing Type. Remixing provides another dimension of application factoring because existing code which knows about existing Types still works without change, but the Types carry along the new fields defined in the remix.
Remixing does not define a new Type. Instead, remix augments an existing Type.
Note: The Type must exist outside the package that remixes it, and must be available in the chain of dependencies as specified in the package's <package name>.c3pkg.json file. Any Type can be remixed except C3 Agentic AI Platform Types.
Since remix does not define a new Type, and instead augments an existing Type, each package can only declare a base Type or a remix Type one time.
Remix a Type
When you remix an existing Type, you create a new .c3typ file, and add the desired fields or methods. Remixing is useful when you do not have access to the original Type file, but want to inherit fields and methods that could be useful.
For example, if a Type WeeklyWeatherData exists in packageA, then this Type can be remixed in packageB by adding new fields to it by using the following syntax:
/**
* WeeklyWeatherData.c3typ
*/
remix type WeeklyWeatherData {
<new fields added here>
}Any existing fields of WeeklyWeatherData are also inherited and are available for use in packageB.
Types marked as final may not be remixed, and core Types in the C3 AI Type System (that mixin Typesys) cannot be remixed.
Remix metadata
Unlike Type definitions, there is no remix declaration for metadata. For example, to extend an existing Transform, the C3 Agentic AI Platform relies on the name field in the metadata to be consistent across packages.
A Transform is a description of how the data is transformed from its raw form (Source) to its integrated form (Target) in a C3 AI database.
For example, if you have a root package:
var data = {
name: "Backblaze-CanonicalBackblaze",
source: "Backblaze",
target: "CanonicalBackblaze",
projection: {
smart_17_raw: "smart_17_raw",
smart_23_normalized: "smart_23_normalized"
}
}And in the remix package:
var data = {
name: "Backblaze-CanonicalBackblaze",
source: "Backblaze",
target: "CanonicalBackblaze",
projection: {
fieldA: "fA",
fieldB: "fB",
fieldC: "smart_17_raw"
}
}The resultant package is:
var data = {
name: "Backblaze-CanonicalBackblaze",
source: "Backblaze",
target: "CanonicalBackblaze",
projection: {
smart_23_normalized: "smart_23_normalized"
fieldA: "fA",
fieldB: "fB",
fieldC: "smart_17_raw"
}
}Note how the value in the name field is consistent in the root package and the remix package.
Remix an existing Type
A remixed Type can also be mixed with other existing Types. For example the remixed Type WeeklyWeatherData can be mixed with the Type WeatherForecast as follows:
/**
* WeeklyWeatherData.c3typ
*/
remix type WeeklyWeatherData mixes WeatherForecast {
<new fields added here>
}Remix in packages
Remixing matters when multiple packages are involved. Consider three packages:
- Package
forecast - Package
shorttermwithdependencies: ["forecast"] - Package
longtermwithdependencies: ["forecast"]
Define the Type WeatherForecast in the package forecast.
/**
* WeatherForecast.c3typ
*/
type WeatherForecast {
precipitation: Precipitation
}Remix the Type WeatherForecast in the package longterm.
/**
* WeatherForecast.c3typ
*/
remix type WeatherForecast {
precipitationProbability: PrecipitationProbability
droughtProbability: DroughtProbability
}And remix the Type WeatherForecast in the package shortterm.
remix type WeatherForecast {
currentTemperature: Temperature
currentWindSpeed: Wind
}The dependencies: ["forecast"] in both longterm and shortterm packages can ensure that all Types in the forecast package, including the WeatherForecast Type, are available for remixing.
Suppose the packages are deployed as follows:
- Deploy the
forecastpackage to ApplicationforecastTag - Deploy the
longtermpackage to ApplicationlongtermTag - Deploy the
shorttermpackage to ApplicationshortTermTag
In this case, the field WeatherForecast.precipitation is available in all three tags as an inherited field.
However, the fields WeatherForecast.precipitationProbability and WeatherForecast.droughtProbability can be available only in the Application longterm.
Similarly, the fields WeatherForecast.currentTemperature and WeatherForecast.currentWindSpeed can be available only in the Application shortTermTag.
How to use mixing
The mixing keyword plays a role in enhancing flexibility in Type declarations, especially within the context of generic programming and Type inheritance.
mixing adds an extra layer of flexibility by allowing the reference to adapt to the most specific or leaf Type in a hierarchy of Types. This means that, when a ReferenceType is set up with mixing, it can automatically adjust its behavior based on the Type it’s actually holding.
Dynamic return Type adaptation
The mixing keyword is commonly used in method declarations to ensure that the method's return Type matches the specific sub-Type calling it. This makes methods in a base Type adapt to the context of the sub-Type without needing to redefine the method at each level.
For example:
// Animal.c3typ
type Animal {
domesticated: boolean = false
// Method convert returns a reference to the leaf type using mixing
domesticate: member function(): mixing Animal js
}
``
```js
// Animal.js
function domesticate() {
return this.withDomesticated(true);
}type Dog mixes Animalvar dog = Dog.make();
dog.domesticated; // returns false
var domesticatedDog = dog.domesticate();
domesticatedDog.domesticated; // returns trueHere, domesticate() is defined in Animal with a mixing keyword, so when it is called from Dog, it returns a Dog instance. When called from Cat, it returns a Cat instance. Without mixing, the method would have to be redefined in each subtype to achieve the same effect.
Working with packages
Order in mixes
Consider the following three packages:
packageOne
abstract type AthleteOne {
ranking: int = 12
}
extendable type PackageOneExtendable schema name "EXTNDONE"packageTwo
abstract type AthleteTwo {
ranking: int = 2317
}
extendable type PackageTwoExtendable schema name "EXTNDTWO"packageThree
dependencies: ["packageOne", "packageTwo"]
abstract type ProfessionalAthlete mixes AthleteOne, AthleteTwo
abstract type AmateurAthlete mixes AthleteTwo, AthleteOneNote that the ordering of Types being mixed in the ProfessionalAthlete Type is different from that of the Type AmateurAthlete. Given the above, the following is true:
ProfessionalAthlete.make().ranking == 12
and
AmateurAthlete.make().ranking == 2317
This is because the order of the Types in the mixes syntax sets the precedence of methods and fields that are duplicates. In the Type ProfessionalAthlete, the field ranking from the Type AthleteOne takes precedence, whereas in the Type AmateurAthlete, the field ranking from the Type AthleteTwo takes precedence.
Example with mixes and extends
Consider the following Type definitions in these packages:
packageFour (deploys successfully)
type MixinExtendable mixes PackageOneExtendableAnd
packageFive (deploys with errors)
type MixinExtendable mixes PackageOneExtendable
type ExtendExtendable extends PackageOneExtendablepackageFour deploys successfully while the package packageFive does not. This is because of the following:
The package packageFive contains two new Types, MixinExtendable and ExtendExtendable, that mixin and extend the same Type, PackageOneExtendable. This is not allowed because:
- In the same package
packageFive, the Type PackageOneExtendable is being extended by two separate parent Types, MixinExtendable and ExtendExtendable at the same time.mixescan be thought of as a particular case ofextendsand you can only extend one Type in a Type definition.
Abstract Types
If you're building a complex application, you may want to define parent Types which are not complete in themselves. These may be marked with the keyword abstract to indicate that they cannot be instantiated directly. In addition to Types being declared as abstract, you can also declare methods as abstract and assume these methods are implemented by sub-Types.
These Types serve the same purpose as super-classes or interfaces in terms of providing encapsulation and shared structure.
Use the keyword abstract before the Type declaration to make the Type abstract. It is not possible to instantiate an instance of an abstract Type.
abstract type MyTypeFinal Types
Declaring a Type as final means that all fields are implicitly final. It cannot be modified or re-implemented by a Type that mixes-in or extends the Type. To declare a Type as final, add the final keyword before Type declaration:
final type SmartBulbImplementation considerations
When using extends
When multiple entity Types are involved, then the extends mechanism of inheritance, supported only on the extendable Types, facilitates a single inheritance model in the C3 Agentic AI Platform.
Note that this extends is the only inheritance model supported between entity Types. Entity Types cannot mix in (with mixes) other entity Types.
Types and data model
Types can share tables as a best practice, but only depending on your data model.
If you have two unrelated data models, then the Types that define these data models are usually not related using extends. Hence the tables that hold these two Types are separate. Fetches from either Type can only return data from that Type and the "isA" relationship is always false between such two Types.
If, however, you have two data models that are related by single inheritance. Then the two Types that define these two data models can be related using extends. For example, if you have a building data model that inherits from a fixed asset data model, then an entity Type Building extends an extendable entity Type FixedAsset. Here a one-way "isA" relationship exists: a Building "isA" FixedAsset. That is, buildings are, by this definition, also fixed assets. However, a FixedAsset "isA" Building relationship does not exist.
So, if you fetch from FixedAsset, you can get all the instances of FixedAsset and Buildings. The fields present in the results of the fetch can be the common fields defined on the FixedAsset Type, for example: name, location.
On the other hand, when you fetch from the Building Type, you can only see entries that are buildings, and they can contain the common fields and any Type-specific fields for Building, for example, numFloors, sqFootage.
When using mixes
The following applies:
- You cannot create an entity Type by mixing-in with other existing entity Types (extendable or otherwise).
- You can only create an entity Type, or a non-entity Type, by mixing-in with other existing non-entity Type.
Calling the parent Type
There are use-cases where you need to access a field or method on the parent Type.
In languages like Java, Python, or JavaScript, the super keyword refers directly to the immediate parent class, allowing the current class to access and call overridden methods and fields from the parent.
In the example below, the Dog Type extends the Animal Type and adds a new property, makeSound. The makeSound method on the Dog Type uses the super keyword to call the parent Type, Animal, and pass in the inputSound argument.
Create the parent Type, Animal. The parent Type has a method makeSound that returns the default sound for all Animal Types.
/**
* Animal.c3typ
* Parent Type
*/
extendable type Animal {
makeSound: function(values: !string): string js
}/**
* Animal.js
* @param inputSound - Input sound
* @returns {string} - Default an animal makes
*/
function makeSound(inputSound) {
return `The default sound for an animal is ${inputSound}`;
}