AMQL Docs
Reusability

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:

  1. Clone the object that is being extended
  2. Create a new extending object from the extending code
  3. 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:

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:

Extracting primitive values to const
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.

Extracting a group of dataset metrics to a PartialDataset
PartialDataset my_dataset {
  description: "This is a dataset that contains metrics a and b"
  metric a {
    // details omitted...
  }
  metric b {
    // details omitted...
  }
}
Extracting a group of model dimensions to a PartialModel
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.

Refer to a const value
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) ;;
  }
}
Refer to a const value with string interpolation
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:

Refer to another model's dimension
Model original_model {
  dimension my_dimension {
    // details omitted...
  }
}

Model another_model {
  dimension another_dimension: original_model.dimension.my_dimension
}
Refer to another dataset's metric
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.

Extend an existing object
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:

Extend an existing object with a Partial
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.

Parameterize logic with Func
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}' ;;
    }
  })
}
Using a Func result as an object
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.

Use cases

On this page