C3 AI Documentation Home

Coding Guidelines

To be consistent with the conventions of the C3 Agentic AI Platform, Types should conform to these guidelines.

Consistency is good not only for itself, but also because it allows people to easily figure out how your Type works.

Adhering to these conventions means that much less typing is necessary as people can quickly look at your Type and figure out how to use it without exhaustive documentation.

References to the Coding Standards appear inline like PS0101.

Philosophy

A designer knows they have achieved perfection not when there is nothing left to add, but when there is nothing left to take away. – Antoine de Saint-Exupery

Prefer DRY ("don't repeat yourself") over WET ("write everything twice", "we enjoy typing" or "waste everyone's time"). See DRY vs WET solutions. PS0001

Our goal is not to create interfaces that can do anything but are hard to use, but rather to create clean and intuitive interfaces that users can use to achieve defined outcomes. If there are meaningful defaults, don't force the user to specify them.

When writing APIs, make sure you have convenience methods that make the common cases easy. For example, design streaming APIs with helper methods and think through the use cases so pipelines can be constructed easily. PS002

Java
   Pair<File, BytePushStream> ps = r.encodedPushStream();
   try (BlockPushStream<Byte, ByteBuffer> s = ps.snd()) {
      s.writeBlock(ths.encodedContent());
   }
   return ps.fst().clearMetadata().readMetadata();

Compare using the lowest-level APIs to:

Java
   return f.writeEncoded(ths.encodedContent()).readMetadata();

Our goal as a platform is to provide maximum leverage and make application code all about the business logic.

Simple things should be simple, complex things should be possible. – Alan Kay

Spec Types

Use spec arguments, which are Types that contain options with defaults, rather than functions with many parameters.

For example:

Type
  /**
   * Fetches multiple obj instances based on a specification.  Only objs that the caller is authorized to fetch will be
   * returned.
   *
   * @param spec
   *           Specification of what data to fetch.  If not specified, no filtering will be applied and a default limit
   *           of 2000 will be applied.
   * @return Requested objs.
   */
  @action(group+='read')
  fetch: function(spec: FetchSpec): !FetchResult<mixing Fetchable>

NOTE: You can use fetch with a small subset of the features provided in the spec and is occasionally used without any spec.

Spec objects should have appropriate default values specified.

NOTE: The system creates a spec if one is not passed so that the implementation can simply use it as is.

The FetchSpec has over a dozen fields, but most often calls look something like this:

JavaScript
  User.fetch({
    filter: Filter.eq('email', email)
  })

Specs are often hierarchical. For example, ValueMergeSpec mixes ValuesEqualSpec and adds more options relevant to merge that are not relevant to equals.

Additional notes on spec Types

  • The spec parameter must be last and named "spec". PS0101 If you are tempted to add more parameters after the spec, add them to the spec object (or a specialized type).
  • A spec parameter is always initialized by the dispatcher, even if the caller does not pass one.
  • The spec Type name should end in "Spec". PS0102
  • The spec Type must mixin Spec. PS0103

Result Types

Types that are defined just to be used as a method return value are called "result types." For example, fetch returns a FetchResult, which contains the list of objects found and other information. Using a result type if a function needs to return multiple values (and not an otherwise distinct type) is highly recommended.

Additional notes on result types

  • The result type name should end in "Result." PS0104
  • The result type must mixin Result. PS0105

Coding style

Use two-space indentations in Type files. PS0201 It is also important not to use tabs within documentation as formatting with tabs breaks when the documentation is extracted. PS0202

Limit lines to 120 characters. PS0203

Documentation comments are important. Formatting markup within comments should be kept to a minimum, but what is there should follow Markdown, not JavaDoc or HTML. PS0205

Type
  /**
   * Construct a Duration from the serialization format.
   *
   * The duration is specified by any combination of integer/unit pairs.
   * Pairs may be separated by spaces or concatenated.
   * For example, "1h 15m" and "75m" both mean 1 hour 15 minutes.
   *
   * The supported units are:
   *  - day (d)
   *  - hour (h)
   *  - minute (m)
   *  - second (s)
   *  - millisecond (ms)
   *
   * If the value is a string containing only a number, the units are assumed to be milliseconds.
   *
   * @param s
   *           Defines the duration as a string (see function description for details).
   * @return the parsed duration
   */
  fromString: function(s: string): Duration js all

Run c3ShowType(Duration) from the Console to see how this translates into live documentation.

Additional notes on coding style

  • One blank line between field/function declarations. PS0204

  • One line between the Type declaration and the first field/function comment, none between the comment and the field/function, and none after the last field/function declaration.

  • When you extend or remix a Type, if a comment is defined on a field/function you override, it overrides the comment on the original field/function.

  • Non-private fields should be documented. PS0206

  • Non-documentation comments (for example, // or /*) should be avoided. PS0207

Types

Document every Type so that it gives others an insight into what the purpose of the Type and how to use the Type. PS0206

Type
/**
 * A function is the declaration of a calling signature without an implementation. It represents an interface that can
 * be called from any language and implemented by a `MethodType method` or `LambdaType lambda`. It is not
 * bound to a type, nor does it necessarily have an implementation.
 *
 * The main information is the set of arguments and the return type, defining the function signature or contract.
 * Parameters are `FunctionParam` instances and the return value is a `ValueType value type`.
 *
 * Parameters have both names and positions.  Language bindings use the natural convention for function calling.
 * Java and JavaScript use position calling and Python using named arguments.
 *
 * Functions are declared using the "function" keyword with the parameters in parenthesis and the return value at the
 * end.  For example the typical "pow" function:
 *
 * ```js
 * function(base: double, exponent: double): double
 * ```
 *
 * Parameters can be declared as non empty by preceding the name with an exclamation point. This will prevent the
 * function from being called without a specified value so the implementation does not need to validate it. Similarly,
 * the return value can be marked as non empty which indicates that the function must return an actual value (or throw
 * an error).
 * 
 * ```js
 * function(base: !double, exponent: !double): !double
 * ```
 *
 * Collections and boolean are assumed to be "non optional"; if no value is specified an empty collection of the
 * appropriate type is passed and unspecified boolean parameters are false. This helps simplify the implementation by
 * removing the necessity for extra checks.
 *
 * Function parameters can have default values.  If a parameter is declared with "`=` *value*", that indicates the
 * declared value will be used if the parameter is not specified.
 * 
 * ```js
 * function(loudness: int = 5)
 * ```
 *
 * This indicates a function which will pass the value `5` to the parameter loudness if the caller does not specify a
 * value.
 *
 * Functions can take variable numbers of arguments.  If the last parameter is declared with `...`, zero or more
 * arguments of that type are allowed. The receiver will get an array of that type (which may be empty). The typical
 * "format" function takes a string plus zero or more values of any type:
 * 
 * ```
 * function(template: !string, values: any ...)
 * ```
 *
 * In this case, the implementation will receive two actual arguments: template and values. The values parameter will
 * be an array of zero or more objects to plug into the template.
 *
 * Member functions are methods where the first implicit parameter is of the containing type and is named "this":
 * 
 * ```c3typ
 * type Cat {
 *   meow: member function(loudness: int)
 * }
 * ```
 *
 * Note that the "this" parameter is implicit, and comes from the `member` declaration. This matches many
 * object-oriented languages that have a concept of an implicit instance on which a method is called.
 *
 * ```js
 * function meow(loudness) {
 *   // this is the instance of Cat
 * }
 * ```
 *
 * ```py
 * def meow(this, loudness):
 *   # this is the instance of Cat
 * ```
 *
 * @see MethodType
 * @see LambdaType
 * @see FunctionParam
 */ 
type FunctionType mixes ValueType, Generic {

}

Additional notes on Types

  • Use two-space indentation (no tabs). PS0201
  • Limit lines to 120 characters. PS0202
  • In comments, use two asterisks in the first line of the comment. PS0207
  • Line up asterisks in subsequent lines with the first one. PS0208
  • Separate paragraphs with a blank line. PS0209
  • For @param put description on a separate line indented three spaces from the start of the parameter name. PS0210
  • Parameter and return value descriptions should be one or more sentence fragments (sentences OK where appropriate) with the first letter of the first word capitalized and terminated with a period. PS0211

Fields

Each new field should also have a documentation comment. PS0206 If you are overriding a field, a new comment is not necessary if the inherited comment is still appropriate. Private fields may or may not need a comment, but it is often better to include a comment.

Type
/**
 * Version history.  Only populated when  db(versionHistory = true).
 * Used for retrieving earlier versions of an Obj.
 *
 * Completely and automatically managed by the system
 */
versionEdits: [VersionEdit]

Additional notes on fields

  • Same basic rules as Type header.

  • No space between field name and colon. PS0212

Methods

Methods are also fields, but have additional conventions because their declarations are more complex. See Method Declaration for more detail.

Type
/**
 * Creates an instance of a C3 type if it doesn't exist or updates it if it does. If the operation fails an
 * exception will be thrown.
 *
 * @param srcObj
 *        If specified, the initial state of obj before any updates.  The actual update to the obj will be only the
 *        diff between the obj and srcObj.  If not specified, the obj will completely replace the existing one if it
 *        doesn't already exist.
 * @param spec
 *           Various parameters that control the operation of function.
 * @return The created or updated obj.  If an include spec is specified in the 'spec.include' field,
 *         then the returned obj will have only those fields populated. Otherwise only the ID field will be populated.
 */
@action(group+='write')
upsert: abstract member function(srcObj: mixing Upsertable, spec: UpsertSpec): mixing Upsertable

Additional notes on methods

  • Same basic rules as for data fields.

  • Required arguments marked with ! in function declaration. PS0213 Don't repeat "required/optional" in the description.

  • For @return, return is implicitly the beginning of the first sentence fragment, so don't capitalize the next word. Otherwise, follow the same rules as for fields.

  • No space between function name and colon. PS0212 No space between closing parenthesis of function argument list and colon.

See references

It is helpful to call out related types or specific fields using the @see annotation. References follow JavaDoc conventions of type#field (or just #field for a field of the same type). PS0214

Type
/**
 * Function description
 *
 * @param arg1
 *           Function argument 1.
 * @return the return value.
 *
 * @see #fieldInMyType
 * @see #functionInMyType
 * @see SomeType
 * @see SomeType#someField
 * @see SomeType#someFunction
 */

Additional notes on references

  • All @see declarations follow @return (if present). PS0215
  • No blank lines between declarations.
  • No text after type/field/function declaration.

Inline references

Also, @link is used to create inline references, which allows specifying different anchor text.

Type
/**
 * Part of implementing a cache is to define a `MetadataListener listener`.
 */

Additional notes on inline references

  • Differs from @see in that it is used inline with other parts of the comment. PS0216
  • Follows same rules as @see for what can be linked and how.
  • Use explicit anchor text after the reference to make the sentence read more naturally.

Metrics

These are some guidelines for naming metrics:

  • The name field does not contain special characters.
  • The id field of a SimpleMetric follows this format: <name>_<srcTypeName>.
  • The name field follows Pascal case.
    • Correct: ThisIsAGoodName
    • Incorrect: thisIsNotGood
    • Incorrect: this_is_not_good
    • Incorrect: This_Is_Not_Good

The following code provides an example of naming a metric:

JSON
{
  "id" : "MeteredElectricityConsumptionSimpleMetric_ServicePoint",
  "name" : "MeteredElectricityConsumptionSimpleMetric",
  "srcType" : "ServicePoint",
  "expression" : "sum(sum(normalized.data.quantity))",
  "path" : "measurements.(measurementType == 'Consumption' && metricName == 'MeteredElectricityConsumption')"
}

Backwards compatibility

The platform attempts to maintain backwards compatibility whenever possible. Methods may change to take additional optional arguments or wider scope and Types may add fields since these are backwards compatible.

If an API cannot be made backwards compatible, it may be deprecated. The deprecation annotation must list the replacement and a final version in which the API is supported. The final version must be at least two minor releases after the deprecation (three-release compatibility).

Type
/**
 * Make widgets.
 */
@deprecated(details="use Sprockets instead", finalVersion="7.8")
makeWidgets: function(): [Widget]

During development within a major release, new APIs may be marked with @beta to indicate that they are not finalized. They may change between minor releases without the normal three-release compatibility.

Additional notes on backwards compatibility

  • Provide a finalVersion with @deprecated. PS0217
  • Indicate the replacement API in details. PS0218

Naming conventions

We use CamelCase (initial capital for Type names PS0106, initial lower case for data fields and methods PS0107) and treat acronyms as words PS0108. For example, "TS" is a common acronym for "time series" and can be used to abbreviate a Type name, such as "TsNormalizer".

Prefixes

Avoid the "get" prefix on accessor functions, preferring to list the property name directly. PS0109 Immutable patterns are preferred, which means that explicit setters are discouraged and methods returning a new instance are used instead. Use the with prefix for such methods.

Type
type User {
  email: string
}

In Java, the API interface generates these methods:

Java
class User {
  String email();
  User withEmail(String email);
}

Avoid "is"/"has" prefixes on data fields, leaving the fields as they would naturally serialize. PS0109

Un-prefixed accessors return the value as stored internally. Accessors prefixed with "as" return the value of the requested Type without conversion or null. They work for values that are assignment compatible and thus are very fast. We refer to this as casting. PS0110

Accessors prefixed with "to" perform conversions, which may fail at run-time because values are sometimes, but not always, convertible. For example, some strings can be converted to integers (parsing), but not all. PS0111

To make it easy for calling code, "as" and "to" accessors generally take extra parameters to throw errors rather than returning null.

Type
  /**
   * Whether or not this field's value type is `string`.
   *
   * @return true if this field is a string
   */
  isString: member function(): boolean

  /**
   * Get the field value if it's a string, otherwise null.
   *
   * @param failIfNot
   *           if true, an error is thrown if the value is not a string
   * @param context
   *           if failIfNot is true, the error thrown includes this context info
   * @return a string if it's the type of this field
   */
  asString: member function(failIfNot: boolean, context: lambda(): string): string

  /**
   * Get the field value if it can be converted to a string, otherwise null.
   *
   * @param failIfNot
   *           if true, null is returned if the value cannot be converted
   * @param context
   *           if failIfNot is true, the error thrown includes this context info
   * @return a string if the value of the field can be converted
   */
  toString: member function(failIfNot: boolean, context: lambda(): string): string

This makes it easy to use these methods if you want appropriate errors to be thrown and keep your code short:

JavaScript
let s = obj.field('email').asString(true, () => 'building mailing list')

If email field is not a string, an error is thrown including the context specified by the caller, such as:

Text
"User.email is not a string while building mailing list."

"from" is the natural converse of "to" and is often useful as a static method on a Type. PS0111

Type
type Url {
  url: !string
  toString:   member function(): !string
  fromString: function(s: !string): !Url
}

The marking of required arguments and return values plus our conventions means this class actually needs very little documentation.

Here is a list of some other common method prefixes and conventions regarding their semantics:

PrefixConventionExampleReference Number
isfor predicate member methodsValueType#isAnyNumberPS0109
no prefix for boolean data fieldsObjBatch.syncPS0112
asfor cast (assignment compatible) member functionsFile.asS3FilePS0111
tofor run-time conversion member methods which may be expensiveVirtualFileSystem#toExternalUrl
fromfor a static method for creation of an Obj from another valueDuration.fromStringPS0111
forfor a static method for a dynamic lookup of an instance of the Type based on key or predicatePageGroup.forUserPS0113
safefor member functions for attributes that depend on existing state but may compute it if missing, or for retrieving references to other persistable / stateful Types that may not have yet been retrieved from back endAbstractContent#safeMd5PS0114
getfor member methods for retrieving state of the current instance from datastore / back-end. It is important to not to use get for simple field accessors to not to mislead user regarding cost of the operation.CloudResource.getResource
listfor static methods for retrieving list of instances from datastore / back-endConfig.listConfigkeys
updatefor a member method for updating current instance in the datastore / back-end. Note that operation will typically fail if instance does not exist in the back/end
upsertfor a member method for updating or creating current instance in the datastore / back-end
ensurefor a member method for retrieving current instance from the back end or for creating it if it does not exists already
setfor a member method for updating some state of a current instance from datastore / back-endPS0117
removefor a member method for deleting some state of a current instance from datastore / back-endPS0118

Methods with name: create vs make

You should only use the term create when creating "state" (for example, database record or file in a file system) not "instance of data" (for example, C3 AI Type instance). Using the term make for the latter is recommended.

Spelling and word choice

Use plurals consistently in field and function names when value type, return type, or argument is a collection. PS0115 (An obvious exception is when name contains a collection name, such as addArray or upsertBatch.)

Type
fields: member function(): [Field]
field:  member function(name: !string, failIfNotFound: boolean, context: lambda(): string): Field

Do not needlessly override already-implemented base functions; accomplish your task by implementing abstract methods.

For example, in MetadataFile, there is no need to override these. PS0219:

Type
read: ~
readString: ~
write: ~
writeString: ~
delete: ~

Additional notes on spelling and word choices

  • Use singular names for data fields and methods of a single value. PS0115

  • Use plural names when the value is a collection. PS0116

  • Avoid repeating the name of the Type in its fields. PS0220

  • Spell acronyms as words to avoid long strings of capital letters. PS0108

  • Don't override methods for no purpose. PS0219

Lambdas

Lambdas are frequently used to streamline the code that uses the Types. There have been examples above (context), but they are also very useful for streaming and chaining interfaces. The advantages of lambdas, introduced in Java 8, are used in these cases:

Java
objs.each(o -> verify(o))

And JavaScript ES6 arrow functions:

JavaScript
objs.each(o => verify(o))

The lambda parameter should generally be last in the function. PS0302 That way, if the value passed spans multiple lines, the other arguments aren't in danger of getting lost.

Additional notes on lambdas

  • Use lambdas to avoid having to write large code with loops. PS0301
  • Lambdas are strongly typed (like Java, not JavaScript).
  • Place lambda parameter last. PS0302

@dependency(include)

Implementation of a method often needs to ensure that certain persistable fields are available in the input object. Typically, Persistable#getMissing is used for this purposes.

However, a more optimal and recommended way to achieve this is to use @dependency(include) annotation on the method declaration.

When @dependency(include) is specified on member methods of Persistable type, the dispatcher ensures that all fields from this included spec are present in the this instance. This avoids need for the member method implementation to hit the database.

Static vs member methods

Typically, if a static method takes an instance as the first required argument and acts upon it, this is an indication of the need for a member method.

Member methods are better because they are easier to call (that is, fewer arguments) and have higher discoverability (for example, code auto-completion).

Static methods are best for constructors or helpers for primitive values (helpers on instances of a C3 AI Type should also be members). Another use case for static methods is a service or API.

Was this page helpful?