Reusability
AML's reusability features, how Extend works, and a step-by-step process for factoring logic out and reusing it across models and datasets.
Introduction
Analytics logic can become increasingly complex and difficult to maintain as more models, datasets and metrics are added. Analysts may end up reinventing the wheel, duplicating efforts and wasting time building similar functionality across different parts of the project.
One of the key design goals of AML (Analytics Modeling Language) is to make the language highly reusable, addressing this common challenge. Unlike pure data serialization formats like XML, JSON, or YAML, AML provides the ability to define reusable components directly within the language.
These reusability mechanisms enable analysts to factor out and abstract repeated logic, making it easy to share and reuse these components across different models and datasets. This helps reduce duplication, improves maintainability, and increases overall efficiency in the analytics development process.
Features
Here is the list of reusability features that AML supports:
- Constant: a value that can be reused throughout a project.
- Function: a reusable block of code designed to perform a specific task.
- Module: a directory containing related AML objects and functions together.
- Extend: a function that can be applied on an analytics object to produce a new object that takes on the original properties.
- Partial: extract shared logic (such as common dimensions or measures) into a reusable object that can be composed into multiple models.
- String interpolation: embed variable values directly into strings.
- If-else: a control-flow expression that conditionally evaluates and returns values, enabling reusable definitions that branch on configuration.
- Custom types: group related settings into one named, type-checked value that can be passed to functions and reused.
Understanding Extend
Extend is the most powerful of these features. It has three key properties:
- Inheritance: When you edit properties in the base objects, these changes propagate to all extending objects.
- Additive: You can add new properties to the extending objects that do not exist in the base objects.
- Overriding: You can modify properties that the extending objects inherit from the base objects, without modifying the original properties.
When you use extend(), AML follows these steps:
- Clone the object that is being extended
- Create a new extending object from the extending code
- Merge and resolve any conflicts between these two objects. If a field is defined in both objects, AML always uses the version in the extending object (overriding)
Example:
Model users {
label: 'Users'
dimension id { ... }
}
Model activatedUsers = users.extend({
label: 'Activated users'
dimension activated_at { ... }
})
// Steps:
// 1. Clone the "users" object into a new copy (users_cloned)
// 2. Create a new extending object from the extending code block
// {
// label: 'Activated users'
// dimension activated_at { ... }
// }
// 3. Merge users_cloned to the newly extending object.
// The 'label' field will be overridden by the new value.
// Final result:
Model {
label: 'Activated users'
dimension id { ... }
dimension activated_at { ... }
}The reusability process
Almost anything created in AML can be defined once and reused by any other AML object. The general steps are:
- Step 1: Identify the logic to be reused and the resulting object that will reuse said logic.
- Step 2: Extract reused logic to a const or a Partial for later reference.
- Step 3: Refer to the reused logic in the resulting object.
- Step 4 (optional): Extend the reused object to further customize it. Useful when you want to mostly reuse the whole object, with some small modifications.
- Step 5 (optional): Parameterize logic with Func, for cases when you need to customize the reused logic with different parameters.
Step 1: Identify the logic to be reused
First identify the logic to be reused, and the resulting object that reuses it. Examples of reused logic include:
- SQL and AQL code snippets, reused in metric and dimension definitions
- A dataset's metrics, reused in other datasets
- A model's dimensions, reused in other models
Step 2: Extract the reused logic to a const or a Partial
Top-level objects in AML with explicit types (Model, Dataset, Metric) are already AML constants and ready to be reused, so no further extraction is needed. An object's properties (a model's dimensions, a dataset's metrics) are also ready to be reused individually (see step 3).
There are two cases where extracting reused logic is necessary:
- For reusing primitive values such as numbers and strings, extract them to a const.
- For reusing parts of an object, extract them to a Partial.
Primitive values can be extracted to const objects:
const my_const = 123
const my_string = "Hello, World!"Multiple properties of objects (such as a dataset's metrics or a model's fields) can be extracted together, at once, to a Partial. Note that any subset of an object's properties can be put in a partial.
PartialDataset my_dataset {
description: "This is a dataset that contains metrics a and b"
metric a {
// details omitted...
}
metric b {
// details omitted...
}
}PartialModel my_model {
dimension a {
// details omitted...
}
dimension b {
// details omitted...
}
}To reuse Partial objects, use the extend function. See step 4.
Use a Module to group extracted objects
Extracted objects can quickly clutter your project files. To improve organization, consider grouping extracted AML objects and functions into a Module: a directory that serves as a container for related items, helping you maintain a cleaner and more structured project.
Step 3: Refer to the reused object or its properties
For primitive values, refer to them directly by name, or use string interpolation.
const high_value_threshold = 10000
Model orders {
// other details omitted...
measure high_value_orders {
label: 'High-Value Orders'
type: 'number'
definition: @aql count(orders.id) | where(orders.amount >= high_value_threshold) ;;
}
}const today = @sql current_date() ;;
Model today_sales {
description: "Sales data for today"
query: @sql select * from sales where date = ${today} ;;
// other details omitted...
}For full objects with explicit names, refer to them directly with the name itself.
For individual properties of objects, use the object's name followed by a dot and the property name:
Model original_model {
dimension my_dimension {
// details omitted...
}
}
Model another_model {
dimension another_dimension: original_model.dimension.my_dimension
}Dataset original_dataset {
metric a {
// details omitted...
}
}
Dataset another_dataset {
metric b: original_dataset.metric.a
}Step 4 (optional): Extend an existing object
Referring to the reused object directly is useful when you want to reuse the whole object. To reuse an existing object with some additional modifications, use extend. It produces a new object that takes on the original properties, while merging in the additional ones.
Model base_model {
dimension a {
// details omitted...
}
}
Model another_model = base_model.extend({
dimension b {
// details omitted...
}
})Another use case is to reuse a Partial. Use extend to combine the Partial with another object:
PartialModel my_partial_model {
dimension a {
// details omitted...
}
}
Model base_model {
// details omitted...
}
Model another_model = base_model.extend(my_partial_model)More details are on the Extend page.
Step 5 (optional): Parameterize logic with Func
When you need to customize the reused logic with different parameters, use Func. A Func is a reusable block of code that takes parameters and returns a value, including a whole AML object.
Func sum(x: Number, y: Number) {
x + y
}
Func double(x: Number) {
const multiple = 2
x * multiple
}
Func users_with_status(status: String) { // return type inferred: Model
users.extend({
label: "Users (${status})"
dimension is_target {
type: 'truefalse'
definition: @sql {{ #SOURCE.status }} = '${status}' ;;
}
})
}Model active_users = users_with_status('active')
Model churned_users = users_with_status('churned')When a function takes many settings, group them into a custom type instead of a long list of parameters. See Create your own types.
More details are on the Func page.