C3 AI Documentation Home

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
 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 keyword member to 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. Replace arg1, arg2, etc., with the actual argument names and ValueType1, 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 are js for JavaScript, py for 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
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: member function
  • input Parameters:
    • purchasePrice: float
    • currentYear: int
  • output Type: float
  • runtimes: js and py

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:

JavaScript
// Automobile.js
function calculateDepreciation(purchasePrice, currentYear) {
    const age = currentYear - this.year;
    const depreciationRate = 0.15;
    return purchasePrice * Math.pow(1 - depreciationRate, age);
};
Python
# Automobile.py
def calculateDepreciation(this, purchasePrice, currentYear):
    age = currentYear - this.year
    depreciation_rate = 0.15
    return purchasePrice * (1 - depreciation_rate) ** age

By 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.

JavaScript
teslaAutomobile.calculateDepreciation(100000, 2020)
Python
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
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:

JavaScript
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
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:

JavaScript
teslaAutomobile.calculateDepreciation(100000)

This call is equivalent to:

JavaScript
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
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
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
type Math {
    min: function(values: int ...): int js
}

This method can be called with any number of int values:

JavaScript
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:

JavaScript
function staticMethod() {
    return "This is a static method for the Automobile Type.";
}
Python
def staticMethod():
    return "This is a static method for the Automobile Type."

Static methods do not require in instance of the Type.

JavaScript
Automobile.staticMethod();
Python
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:

JavaScript
function memberMethod() {
    return `Model: ${this.model}, Year: ${this.year}, Color: ${this.color}`;
}
Python
def memberMethod(this):
    return `Model: ${this.model}, Year: ${this.year}, Color: ${this.color}`

You can call these methods as follows:

JavaScript
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();
Python
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.

Type
// AutomobileService.c3typ
type AutomobileService {
    service: abstract function(auto: Automobile): string js
}
Type
// DetailedAutomobileService.c3typ
type DetailedAutomobileService mixes AutomobileService {
    service: ~
}
JavaScript
function service(auto) {
    return `Detailed Automobile Service for: ${auto.model}, ${auto.year}`;
}
Python
def service(cls, auto):
    return f"Servicing the automobile: {auto.model}, Year: {auto.year}"

Note: ~ implies re-implementing something from a parent Type.

JavaScript
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);
Python
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.

Type
// Maintenance.c3typ
type Maintenance {
    maintenanceCheck: optional function(auto: Automobile): string js
}
Type
// 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
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.

JavaScript
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;
}
JavaScript
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));
Python
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.0
Python
purchase_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.

Type
// 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
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:

Text
@action(authzChildActions=true)
function 1
└── inline function 2
    └──function 3

In 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.

SymbolDefinition
!Required non-null value
!?Required value, null ok
=Specify a default value
Type
// 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.

JavaScript
// 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
}
Python
# 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 depreciation

Cached

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
type Automobile {
    // Cached method to calculate the depreciation of the automobile.
    calculateDepreciation: cached member function(years: int): double
}
JavaScript
var auto = Automobile.make({
    model: "Porsche",
    year: 2024,
    color: "white"
});
JavaScript
auto.calculateDepreciation(5); // Calculates and caches the result
auto.calculateDepreciation(5); // Returns the cached result, does not recalculate
Python
auto = c3.Automobile(
    model="Tesla X",
    year=2024,
    color="red")
Python
print(auto.calculateDepreciation(5))  # Calculates and caches the result
print(auto.calculateDepreciation(5))  # Returns the cached result, does not recalculate

The 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
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.

Was this page helpful?