C3 AI Documentation Home

The C3 AI Way

These principles help drive the development of the C3 Agentic AI Platform and should be adhered to by all developers across the platform, from the innermost platform components to data scientists and application developers.

There are 10 core principles that are derived from experience making complex systems fast and reliable.

  1. Fast by default
  2. Everything is a Type
  3. Simple things should be easy
  4. Every instruction counts
  5. Don't be selfish
  6. No copy/paste
  7. Use functional patterns
  8. Document in code
  9. Make reusable building blocks
  10. Test utils mean missing APIs

Fast by default

If there is an alternative solution that requires more work (CPU, memory or DB), make it optional. For example, sorting the results of a find method should be done only if the user requests it.

This principle relates to Make reusable building blocks in that smaller operations can be chained to obtain the desired functionality without having to bulk out the implementation.

Everything is a Type

It is important to fully declare all data structures as C3 AI Types. This is not only for documentation purposes, but also for error checking and efficiency.

Using generic types such as any or json results in the contents of the value being documented elsewhere. Or worse, not documented at all. It also prevents the platform from making optimizations based on the actual data type.

There are some helpful Types such as Pair and TupleType which help avoid having to declare a whole new Type to hold several values while still maintaining strong typing.

Simple things should be easy

A complex system often has a complex API. However, it is important to strive to make sure the most-common use case is natural and easy to use.

Simple things should be simple, and complex things should be possible. ~ Alan Kay.

For example, the following suggestions could simplify functions, which contain too many parameters in the function signature:

  • Break the logic of the function into separate functions.
  • Express the function with key parameters, including any additional parameters as optional Spec.
  • Use Overloads to simplify the function with few (or no) parameters.
  • Ensure parameters and specs have appropriate default values. For example, Collection#withoutOutliers defaults to ±2σ, so it can be used to perform simple filtering.
  • Break out logic into separate functions if there are too many parameters compiled into a function signature.

Every instruction counts

Performance problems can be a critical issue in the platform, such as algorithms with bad growth orders, but they can also be small inefficiencies (death by a thousand cuts). The latter are actually harder to find and fix because they are not always visible during profiling. That's why it is critical to be aware and not introduce them in the first place.

For example, avoid an extra check for a null argument value at the top of a method implementation if later on the system throws a NPE. If an input value should never be null, include this check in the declaration by marking the parameter as required (foo: !string) and then you don't need the check at all.

Don't be selfish

If you find yourself writing a useful utility, don't bury it inside an implementation. There are standard places for reusable utility methods so the code can be written once and used many times.

Text
- `Bool`: Boolean conversion and logic
- `Char`: Unicode Character predicates and conversions
- `Env`: system Environment utilities
- `Glob`: UNIX style path patterns
- `Io`: Input/Output (local file system, Java resources, etc)
- `Jsn`: JSON parsing and building
- `Num`: Numeric utilities
- `Os`: Operating System utilities
- `Regex`: Regular Expression helpers
- `Str`: String processing utilities
- `Val`: Value predicates, merging, transformation and conversion
- `Xml`: XML parsing and building

Note that the pattern for conversions is to start with what you have and look for a function that converts it to what you need. For example, Num.toJson() (rather than Jsn.fromNumber()).

Avoid copy and paste

When adding functionality, make sure not to copy an existing piece of code and modify it. It's better to refactor or generalize the code to handle both use cases and call the code from both locations. This reduces the amount of code, improves performance since it decreases the number of functions added to the call stack, and also means future enhancements and bug fixes are less painful since there is only a single location where the core logic must be modified.

Use functional patterns

Rather than using language looping constructs, use functional programming patterns. Not only does this make the code simpler, but it allows the platform to perform optimizations because the scope is well-defined.

For example, one might write this loop:

Java
int valid = 0;
for (double v : values) {
  if (Double.isFinite(v))
    valid++;
}

Look how much easier the functional equivalent is:

Java
int valid = values.filter(v -> Double.isFinite(v)).size();

The above code snippet is less complex and more concise compared to the legacy style of coding. The data is being transformed from one function to another instead of being mutated.

Document in code

When defining your Types make it a habit to use documentation comments on the Types, fields and methods. Not only does this put the documentation with the code, making it more likely that both are maintained together, but also means the platform can show this documentation during development and at the point of use.

It is paramount to not store documentation in external places, such as emails, wikis or file sharing services, because they get out of date or out of sync and not managed along with the code. If the documentation is in the Type and implementation files, it remains in sync with the codebase so there is no issue about what version the documentation is referencing.

The C3 Agentic AI Platform supports separate documentation topic files (.c3doc.md like this one) which are in a src/doc directory and managed along with the code for consistency. However, by being separate, they are more natural for larger topics than API documentation.

There are only three locations documentation is encouraged:

Text
- reference documentation: `.c3typ` files
- expository documentation: `.c3doc.md` files
- implementation details: source code (`.java`, `.js` and `.py`) and `README.md` files

Note that all these are with the code (managed by Git) so there is no question about what is current or applies to a given version.

Make reusable building blocks

Rather than complex functions, it's better to have functionality in small pieces that can be put together in different ways. This is related to Use functional patterns in that it provides a natural way to express things and accumulate powerful behavior from simple, reusable pieces.

Of course a key benefit is expressive power: more things can be done than the provider of the API imagined beforehand. Another important benefit is optimization: functional pattern optimizations such as delaying operations until collection happen without having to be implemented each time.

Test utils mean missing APIs

If you find yourself writing a "helper" class or function to implement your test, that probably means you are missing a natural API.

This relates also to Simple things should be easy because most likely you're missing a simpler form with good defaults that would be more natural to use.

Of course, if you find yourself writing a "helper" class or method in either the implementation or test, that may also mean it should be in a shared utility class, Don't be selfish.

Was this page helpful?