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
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:
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:
/**
* 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:
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
/**
* 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 allRun 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
/**
* 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
@paramput 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.
/**
* 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.
/**
* 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 UpsertableAdditional 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
/**
* 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.
/**
* 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
namefield does not contain special characters. - The
idfield of a SimpleMetric follows this format:<name>_<srcTypeName>. - The
namefield follows Pascal case.- Correct:
ThisIsAGoodName - Incorrect:
thisIsNotGood - Incorrect:
this_is_not_good - Incorrect:
This_Is_Not_Good
- Correct:
The following code provides an example of naming a metric:
{
"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).
/**
* 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
finalVersionwith@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 User {
email: string
}In Java, the API interface generates these methods:
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.
/**
* 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): stringThis makes it easy to use these methods if you want appropriate errors to be thrown and keep your code short:
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:
"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 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:
| Prefix | Convention | Example | Reference Number |
|---|---|---|---|
| is | for predicate member methods | ValueType#isAnyNumber | PS0109 |
| no prefix for boolean data fields | ObjBatch.sync | PS0112 | |
| as | for cast (assignment compatible) member functions | File.asS3File | PS0111 |
| to | for run-time conversion member methods which may be expensive | VirtualFileSystem#toExternalUrl | |
| from | for a static method for creation of an Obj from another value | Duration.fromString | PS0111 |
| for | for a static method for a dynamic lookup of an instance of the Type based on key or predicate | PageGroup.forUser | PS0113 |
| safe | for 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 end | AbstractContent#safeMd5 | PS0114 |
| get | for 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 | |
| list | for static methods for retrieving list of instances from datastore / back-end | Config.listConfigkeys | |
| update | for 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 | ||
| upsert | for a member method for updating or creating current instance in the datastore / back-end | ||
| ensure | for a member method for retrieving current instance from the back end or for creating it if it does not exists already | ||
| set | for a member method for updating some state of a current instance from datastore / back-end | PS0117 | |
| remove | for a member method for deleting some state of a current instance from datastore / back-end | PS0118 |
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.)
fields: member function(): [Field]
field: member function(name: !string, failIfNotFound: boolean, context: lambda(): string): FieldDo 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:
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:
objs.each(o -> verify(o))And JavaScript ES6 arrow functions:
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.