Defining Methods in the C3 AI Type System
Methods are functions associated with a Type that define specific actions or behaviors the Type can perform. They are declared within a Type, similar to how you declare fields, but a method's ValueType is a FunctionType. Methods can be member methods, which operate on instances of a Type, or static methods, which operate on the Type itself. Methods can also be marked with modifiers such as abstract for methods to be implemented by sub-Types, optional for methods that may not be implemented, and final for methods that cannot be overridden.
Method syntax
Declare methods using C3's domain-specific language (DSL) and implement them in any programming language compatible with the C3 Agentic AI Platform.
Specify the method name as a field on the Type in camel case with comma-separated input arguments. For each input argument, list the argument name first, followed by a colon, and then the data type. After listing all input parameters, add another colon, followed by the output Type. Finally, specify the implementation runtime after the output Type.
type Automobile {
<method name>: <optional modifier> function(arg1: ValueType1, arg2: ValueType2, ...) : <return ValueType> <runtime claim>
}<method name>: This specifies the name of the method. It should be written in camel case. Replace<method name>with the actual name of your method.<optional modifier>: A modifier such as the keywordmemberto indicate the function is member function.function: This keyword indicates that what follows is a function definition.arg1: ValueType1, arg2: ValueType2, ...: These are the input arguments for the function. Each argument is listed by its name followed by a colon and its data type. You can have multiple arguments separated by commas. Replacearg1, arg2, etc., with the actual argument names andValueType1, ValueType2, etc., with the actual data types.:: This colon separates the input parameters from the return value type.<return ValueType>: This specifies the return type of the function. Replace<return ValueType>with the actual data type of the return value.<runtime claim>: This specifies the runtime environment in which the function is implemented. Common examples arejsfor JavaScript,pyfor Python. Replace<runtime claim>with the actual runtime environment for your function.
This template provides a clear structure for defining methods in the C3 AI Type System, allowing you to specify function signatures and their implementation details succinctly.
Method declaration
Now, let's look at how to apply this syntax in a concrete example by declaring a method within the Automobile Type.
The member function calculateDepreciation calculates the depreciation value of the automobile based on its purchase price and the current year.
type Automobile {
model: string
year: int
color: string
calculateDepreciation: member function(purchasePrice: float, currentYear: int): float js | py
}Here are the properties of the calculateDepreciation method:
- method Name:
calculateDepreciation - type:
memberfunction - input Parameters:
purchasePrice:floatcurrentYear:int
- output Type:
float - runtimes:
jsandpy
A method implementation must be associated with the Type declaring the method. For example, the calculateDepreciation method should be implemented in a file called Automobile.js or Automobile.py, depending on the runtime environment.
Here is an example of the implementation in both JavaScript and Python:
// Automobile.js
function calculateDepreciation(purchasePrice, currentYear) {
const age = currentYear - this.year;
const depreciationRate = 0.15;
return purchasePrice * Math.pow(1 - depreciationRate, age);
};# Automobile.py
def calculateDepreciation(this, purchasePrice, currentYear):
age = currentYear - this.year
depreciation_rate = 0.15
return purchasePrice * (1 - depreciation_rate) ** ageBy declaring both js and py in the method claim, the system knows that there are valid implementations in both languages, and it can choose the appropriate implementation based on the runtime environment.
Claims may include not just the language but also specific environments or libraries required for execution. For a deeper understanding of Action.Requirement, ImplLanguage.Runtime, and ImplLanguage, refer to the additional documentation on method claims in the Runtimes and Execution section.
teslaAutomobile.calculateDepreciation(100000, 2020)teslaAutomobile.calculateDepreciation(100000, 2020)Input parameters and positional arguments
When defining methods in the Type System, it is important to understand how input parameters work, including the use of positional arguments and default values.
Positional arguments
Input parameters for methods are typically defined as positional arguments. This means that when the method is called, the arguments must be provided in the exact order they are declared. The system will match each argument to its corresponding parameter based on position.
For example, consider the following method declaration:
type Automobile {
calculateDepreciation: function(purchasePrice: float, currentYear: int): float js
}Here, purchasePrice is the first positional argument and currentYear is the second. When calling this method, the first value provided is used for purchasePrice and the second for currentYear:
teslaAutomobile.calculateDepreciation(100000, 2020)In this example, 100000 is passed to purchasePrice and 2020 is passed to currentYear.
Default values for parameters
In addition to positional arguments, you can also define default values for input parameters. This allows the method to be called without explicitly providing all arguments, as any missing arguments are set to their default values.
Default values are specified using the = sign after the parameter type:
type Automobile {
calculateDepreciation: member function(purchasePrice: float, currentYear: int = 2024): float js | py
}In this example, if currentYear is not provided when the method is called, it defaults to 2024:
teslaAutomobile.calculateDepreciation(100000)This call is equivalent to:
teslaAutomobile.calculateDepreciation(100000, 2024)Mixing required and optional parameters
When defining methods, you can mix required and optional parameters. Required parameters must be provided by the caller, while optional parameters can be omitted if they have a default value.
For instance:
type Automobile {
calculateDepreciation: member function(purchasePrice: !float, currentYear: int = 2024): !float js | py
}Here, purchasePrice is a required parameter (indicated by the ! prefix), while currentYear is optional. If purchasePrice is not provided, the system raises an error. However, if currentYear is omitted, it defaults to 2024.
Order of parameters
The order in which parameters are declared is crucial, especially when mixing required and optional parameters. Typically, required parameters should be declared first, followed by optional ones with default values. This ensures that positional argument matching works correctly and avoids ambiguity:
type Automobile {
calculateDepreciation: function(purchasePrice: !float, currentYear: int = 2024, depreciationRate: float = 0.15): !float js
}In this example, purchasePrice must be provided, while currentYear and depreciationRate are optional, defaulting to 2024 and 0.15, respectively.
Variable number of parameters
In some cases, you may want to define a method that accepts a variable number of arguments. This is done using the ... syntax, which denotes that the method can accept multiple values of a certain type:
type Math {
min: function(values: int ...): int js
}This method can be called with any number of int values:
Math.min(3, 5, 1, 9)Here, values is treated as an array of integers, and the method can process all provided arguments.
Method types
Static methods
You can define static methods to perform operations that are relevant to the Type as a whole rather than a specific instance. For example, you might want to define a method that returns a description of the Automobile Type:
function staticMethod() {
return "This is a static method for the Automobile Type.";
}def staticMethod():
return "This is a static method for the Automobile Type."Static methods do not require in instance of the Type.
Automobile.staticMethod();c3.Automobile.staticMethod()Member methods
Member methods operate on instances of the Type. For instance, you could define a method to return a formatted description of an automobile instance:
function memberMethod() {
return `Model: ${this.model}, Year: ${this.year}, Color: ${this.color}`;
}def memberMethod(this):
return `Model: ${this.model}, Year: ${this.year}, Color: ${this.color}`You can call these methods as follows:
var tesla = Manufacturer.make({
name: "Tesla"
});
var teslaAutomobile = Automobile.make({
model: "Tesla X",
year: 2024,
color: "red",
manufacturer: tesla,
automobileCategory: AutomobileCategory.SPORT_CAR
})
;
teslaAutomobile.memberMethod();tesla = c3.Manufacturer(name="Tesla")
teslaAutomobile = c3.Automobile(model="Tesla X",
year=2024,
color="red",
manufacturer=tesla,
automobileCategory=c3.AutomobileCategory.SPORT_CAR)
teslaAutomobile.memberMethod()The this keyword
The this keyword has special meaning in C3 AI JavaScript methods. In the context of C3 AI member methods, this is the Type instance on which the member method was invoked. In the context of C3 AI static (non-member) methods, this is the Type upon which the method was invoked.
Note: You may have a case where you want to use static calling of methods on a class, but need the state provided by an instance. For this purpose, your Type can mixin DefaultInstance and implement the inst method to return the canonical or default instance. Member methods can be invoked on Types that mixin DefaultInstance in static style and the system calls inst() first, then calls the member method with the value returned as the context (this).
Abstract methods
Normally, a method declaration indicates the declaring Type is providing the implementation, but if the Type is defining an interface, it can declare the entire Type or specific methods as abstract. A Type that mixes in the abstract Type must implement the methods defined by the parent Type, or re-declare the methods as abstract.
Abstract methods are those that are not implemented by the declaring Type but rather by the Type that mixes it in. They are useful for defining interfaces.
// AutomobileService.c3typ
type AutomobileService {
service: abstract function(auto: Automobile): string js
}// DetailedAutomobileService.c3typ
type DetailedAutomobileService mixes AutomobileService {
service: ~
}function service(auto) {
return `Detailed Automobile Service for: ${auto.model}, ${auto.year}`;
}def service(cls, auto):
return f"Servicing the automobile: {auto.model}, Year: {auto.year}"Note: ~ implies re-implementing something from a parent Type.
var tesla = Manufacturer.make({
name: "Tesla"
});
var teslaAutomobile = Automobile.make({
model: "Tesla X",
year: 2024,
color: "red",
manufacturer: tesla,
automobileCategory: AutomobileCategory.SPORT_CAR
});
DetailedAutomobileService.service(teslaAutomobile);tesla = c3.Manufacturer(name="Tesla")
teslaAutomobile = c3.Automobile(model="Tesla X",
year=2024,
color="red",
manufacturer=tesla,
automobileCategory=c3.AutomobileCategory.SPORT_CAR)
c3.DetailedAutomobileService.service(teslaAutomobile)Optional methods
The optional modifier indicates a weaker flavor of method declaration. Optional methods are similar to abstract methods because the declaring Type does not implement them. However, unlike abstract methods, it is not assumed that the first Type mixing in the optional method implements it. In fact, an optional method is not considered implemented unless it is explicitly re-declared.
Optional methods are similar to abstract methods, but they must be re-declared to be implemented.
// Maintenance.c3typ
type Maintenance {
maintenanceCheck: optional function(auto: Automobile): string js
}// BasicMaintenance.c3typ
type BasicMaintenance mixes Maintenance {
maintenanceCheck: ~
}Both member and static functions can be abstract or optional.
Private methods
Private methods do not appear in documentation and can not be called from outside their package. They typically handle internal implementations while keeping the internal state hidden.
type Automobile {
calculateInternalValue: private member function(purchasePrice: float, age: int, mileage: int, conditionRating: float, marketTrends: float, maintenanceCosts: float): float js | py
}Running the c3ShowType(Automobile) in the console or help(c3.Automobile) in a Jupyter Notebook will show documentation for the Automobile Type, however, the documentation for the calculateInternalValue method is hidden.
function calculateInternalValue(purchasePrice, age, mileage, conditionRating, marketTrends, maintenanceCosts) {
const baseValue = purchasePrice * 0.5; // Ensuring the Automobile retains at least 50% of its value
const depreciationFactor = (age * 0.02) + (mileage / 250000) + ((10 - conditionRating) * 0.02);
const depreciationAmount = purchasePrice * depreciationFactor;
const marketAdjustment = purchasePrice * marketTrends;
const internalValue = (baseValue - depreciationAmount + marketAdjustment) - maintenanceCosts;
return internalValue > 0 ? internalValue : 0;
}let purchasePrice = 30000; // $30,000
let age = 5; // 5 years old
let mileage = 60000; // 60,000 miles
let conditionRating = 8.5; // Condition rating of 8.5 out of 10
let marketTrends = 0.02; // 2% increase in value due to market trends
let maintenanceCosts = 2000; // $2,000 in maintenance costs
let internalValue = auto.calculateInternalValue(purchasePrice, age, mileage, conditionRating, marketTrends, maintenanceCosts);
console.log("Internal Value of the Car: $" + internalValue.toFixed(2));def calculateInternalValue(self, purchase_price, age, mileage, condition_rating, market_trends, maintenance_costs):
base_value = purchase_price * 0.5 # Ensuring the Automobile retains at least 50% of its value
depreciation_factor = (age * 0.02) + (mileage / 250000) + ((10 - condition_rating) * 0.02)
depreciation_amount = purchase_price * depreciation_factor
market_adjustment = purchase_price * market_trends
internal_value = (base_value - depreciation_amount + market_adjustment) - maintenance_costs
return internal_value if internal_value > 0 else 0.0purchase_price = 30000 # $30,000
age = 5 # 5 years old
mileage = 60000 # 60,000 miles
condition_rating = 8.5 # Condition rating of 8.5 out of 10
market_trends = 0.02 # 2% increase in value due to market trends
maintenance_costs = 2000 # $2,000 in maintenance costs
internal_value = teslaAutomobile.calculateInternalValue(purchase_price, age, mileage, condition_rating, market_trends, maintenance_costs)
print(f"Internal Value of the Car: ${internal_value:.2f}")Final methods
Final methods cannot be overridden by a mixin. They are useful to ensure certain behaviors cannot be changed by other Types.
// Car.c3typ
type Car {
calculateValue: member function(): int js
checkEngine: final member function(): string js
}Inline methods
When you mark a method as inline, the C3 Type System treats the action as a native function call and explicitly opts out of C3 AI handling. Inline methods skip overhead processes such as the following:
- Parameter validation
- Return value validation
- Logging
- Authorization checks
You might want to mark a method as inline in the following scenarios:
- Methods called frequently or loop methods that you want to speed up
- Simple methods that you want to duplicate, such as string manipulation tasks or util functions
Use inline only for trivial methods, usually those that perform quick operations on their arguments. Because inline functions skip authorization checks, they can inadvertently expose sensitive information. If an inline method contains security checks or sensitive logic, marking the method as inline could make this logic more accessible to an attacker. An appropriate example of an inline method is the User#myUser method, because the caller of the function is already authorized.
You can also apply the inline modifier at the Type level, which marks all declared methods in that Type as inline. However, this behavior does not automatically apply to Types that mix in an inline Type; if you want the child Type to be inline, you must declare it explicitly.
Here's an example of how to mark a method as inline:
type Automobile {
getModel: inline member function(): string js
}Inline method behavior in an action stack
An action stack is the call stack of C3 AI Action invocations. The platform does not add inline methods to the action stack, and treats inline methods as part of the action that called it.
Consider the following action stack where inline function 2 is an inline method:
@action(authzChildActions=true)
function 1
└── inline function 2
└──function 3In this action stack, function 1 has the authzChildActions annotation, which requests re-authorization for all actions called by function1. inline function 2 skips authorization checks because it is inline. Although function 3 is a child action of inline function 2, function 3 does not skip authorization checks, and the platform enforces permission checks related to the authzChildActions annotation. A user must have explicitly defined permissions to run function 1 and function 3, but not to run inline function 2.
If you call C3.action to retrieve the current running action, the platform returns the highest level action that is not an inline method. This behavior occurs because the platform does not acknowledge inline methods in an action stack. In the previous action stack example, if you call C3.action on inline function 2, it returns function 1 even if you called C3.action on inline function 2.
Required parameters
Marking parameters or return values as required allows the platform to perform validation of these parameters automatically. Just like for Type fields, method parameters and return values may be prefixed with a ! to indicate that they must have a value (for example, non-null) supplied by the caller to be evaluated.
Normally, non-required parameters are completely optional. However, !? can be used to indicate that a parameter must be specified, but may be null. This is useful in situations where not passing the value is a coding error (the function makes no sense without that argument), but null is a valid value. For example, putting a value into a map does not require a value (null is allowed), but it is a misuse if a value is not specified at all.
Parameters may also have a default value using the = sign, which indicates that if the argument is not specified for that parameter, the default value is used.
| Symbol | Definition |
|---|---|
| ! | Required non-null value |
| !? | Required value, null ok |
| = | Specify a default value |
// file name: Automobile.c3typ
type Automobile {
model: string
year: int
color: string
calculateDepreciation: function(years: !int): !double py-client
}In this example, the calculateDepreciation method has one parameter, years, a required non-null integer.
// Implementing calculateDepreciation method with required parameter in JavaScript
function calculateDepreciation(years) {
if (years === null || years === undefined) {
throw new Error("years is required"); // Throwing an error if years is not provided
}
const depreciationRate = 0.15; // Setting the depreciation rate per year
return this.year * (1 - depreciationRate * years); // Calculating the total depreciation
}# Implementing calculateDepreciation method with required parameter in Python
def calculateDepreciation(this, years):
if years is None:
raise ValueError("years is required") # Throwing an error if years is not provided
depreciation_rate = 0.15 # Setting the depreciation rate per year
return this.year * (1 - depreciation_rate * years) # Calculating the total depreciationCached
In the Type System, a method can be marked as cached to indicate that its return value should be stored (cached) after the first execution. This is useful for methods that perform expensive calculations or operations where the result does not change often. Once the method is called, the result is stored, and subsequent calls return the cached value without recalculating.
In the example below, the calculateDepreciation method of the Automobile Type is marked as cached. The result of the depreciation calculation is stored after the first call, so subsequent calls with the same yearsOwned value returns the cached result.
type Automobile {
// Cached method to calculate the depreciation of the automobile.
calculateDepreciation: cached member function(years: int): double
}var auto = Automobile.make({
model: "Porsche",
year: 2024,
color: "white"
});auto.calculateDepreciation(5); // Calculates and caches the result
auto.calculateDepreciation(5); // Returns the cached result, does not recalculateauto = c3.Automobile(
model="Tesla X",
year=2024,
color="red")print(auto.calculateDepreciation(5)) # Calculates and caches the result
print(auto.calculateDepreciation(5)) # Returns the cached result, does not recalculateThe first time the calculateDepreciation function is called with a specific years value, it calculates the result, caches it, and returns the result.
The second time the calculateDepreciation function is called with the same years value, it returns the cached result without recalculating.
Stateful
A stateful method in the Type System indicates that the method depends on the internal state of the object or has side effects that prevent its results from being cached or used in certain contexts (for example, idempotent operations). Stateful methods are typically used when the result varies depending on the instance's current state or when the method modifies the state of the instance.
type Automobile {
// Stateful method to update the mileage of the automobile.
updateMileage: stateful member function(newMileage: int)
}Here, the updateMileage method of the Automobile Type is marked as stateful because updating the mileage changes the internal state of the Automobile object.
Two use-cases for when you can use the stateful modifier are:
- Database query with side effects:
A function that queries a database and also updates a field or logs an entry could be considered stateful, as the state of the database or the logs might change with each call.
- Time-dependent operations:
A function that returns the current time or performs actions based on the current time is stateful because the result can vary depending on when the function is called.