AML Types
This document describes the built-in types supported by AML, and how to define your own custom types
Introduction
AML includes a powerful type system that enables both a real-time feedback to analysts to help them avoid errors early, and as the basis for sophisticated reusability features such as constant declaration, functions, and extend.
Basic types
Number
- All kind of numbers (including integer and fractional)
Number pi = 3.14159265
Number b = 2
Number c = -10Int
- Represents numbers without decimal
Int a = 1
Int b = 2
Int c = -100Boolean
Boolean is_truthy = true
Boolean is_falsy = falseString
A string is a sequence of characters used to represent text.
AML supports 2 kind of strings:
- normal string (single/double quotes)
- multi-line string (triple quotes)
const model_name = 'ecommerce.users' // single-quote
const dataset_name = "ecommerce" // double-quote
// Multi-line string
const note = '''
This is
a note
that expands
on multiple
lines
'''
// "This is\na note\nthat expands\non multiple\nlines"List
- Stores a collection of elements, typically of the same type
- Syntax:
List[T](whereTis element’s type)
Type ListInt = List[Int]
Type ListString = List[String]
ListInt numbers = [1, 2, 3]
ListString names = ['Alex', 'John', 'Bob']
// access elements inside a list
numbers(0) // 1
numbers(1) // 2
numbers(2) // 3
names(1) // 'John'Dictionary
- Stores a collection of key-value pairs
- Each key-value pair maps the key to its associated value
- Syntax:
Dict[K, V]whereKis a key’s type,Vis a value’s type
Type NumberDict = Dict[String, Number]
NumberDict employee_ages = {
alex: 23
bob: 30
john: 29
}
Type StringDict = Dict[String, String]
StringDict employee_addresses = {
alex: '505 SW 1st Ave'
john: ' 4070 Red Arrow Hwy'
bob: '851 St Georges Ave'
}
// access dictionary entries
employee_ages('alex') // 23
employee_addresses('bob') // '851 St Georges Ave'Any
- is a type that can hold any value, effectively opting out of type checking
Custom types
Besides the basic types like String and Number, AML lets you define your own types. A custom type (also called a record type) groups a few related values under one name, and gives each value a field name and a type.
To see why that helps, imagine a function that adds a segment flag (such as "is a VIP customer") to any model:
Func with_segment(base: Model, label: String, column: String, value: String, hidden: Boolean) => Model {
base.extend({
dimension in_segment {
label: label
type: 'truefalse'
hidden: hidden
definition: @sql {{ #SOURCE.${column} }} = '${value}' ;;
}
})
}
Model vip_users = with_segment(users, 'Is VIP', 'tier', 'vip', false)
Model trial_users = with_segment(users, 'On trial', 'plan', 'trial', true)It works, but each call is a row of unnamed values. It's hard to tell which string is the column and which is the value, it's easy to swap two of them by accident, and every call has to repeat all of them.
With a custom type, you describe the settings once and pass them around as one value:
Type SegmentSettings {
label: String
column: String
value: String
hidden (default: false): Boolean
}
Func with_segment(base: Model, segment: SegmentSettings) => Model {
base.extend({
dimension in_segment {
label: segment.label
type: 'truefalse'
hidden: segment.hidden
definition: @sql {{ #SOURCE.${segment.column} }} = '${segment.value}' ;;
}
})
}
Model vip_users = with_segment(users, SegmentSettings {
label: 'Is VIP'
column: 'tier'
value: 'vip'
})
Model trial_users = with_segment(users, SegmentSettings {
label: 'On trial'
column: 'plan'
value: 'trial'
hidden: true
})Every value now has a label, fields you skip fall back to their defaults, and AML underlines a misspelled or missing field right in the editor.
Syntax
Declare a custom type with the Type keyword, followed by its name and a list of fields:
Type <TypeName> {
<field_name>: <field_type>
<field_name> (optional): <field_type>
<field_name> (default: <value>): <field_type>
}- By convention, type names use
PascalCase(e.g.SegmentSettings), and field names usesnake_case. - A field can use any type: a built-in type, a literal like
'asc', a union of values, or another custom type. - A field is required unless you mark it
(optional)or give it a(default: ...)value. These are two of AML's field rules, which also let a field accept repeated blocks.
Once declared, the type name works in two ways. You can use it to declare a named value, or call it like a constructor to create a value inline:
// Declare a named value
SegmentSettings vip_segment {
label: 'Is VIP'
column: 'tier'
value: 'vip'
}
// Create a value inline, for example as a function argument
with_segment(users, SegmentSettings { label: 'On trial', column: 'plan', value: 'trial' })To read a field, use a dot: vip_segment.label returns 'Is VIP'.
Optional fields and default values
A field marked (optional) can be left out. A field with (default: ...) can also be left out, and AML fills in the default for you.
Type KpiSettings {
title: String
format (default: '#,###'): String
color (optional): String
}
KpiSettings orders_settings {
title: 'Orders'
}
orders_settings.format // '#,###' (filled in from the default)Prefer defaults for fields you read
If your code reads a field, prefer a default value over (optional). An optional field that was left out has no value, so reading it gives you nothing to work with. A default guarantees the field always has something sensible.
Limit a field to a few values
Sometimes a field should only accept a few specific values. List them with | (read it as "or"):
Type SortRule {
field: String
direction: 'asc' | 'desc'
}
SortRule by_revenue {
field: 'revenue'
direction: 'up' // error: Type 'String' is not assignable to type '"asc" | "desc"'
}If you use the same set of values in several places, give it a name with Type <Name> = .... This creates a type alias, a shorter name for an existing type:
Type SortDirection = 'asc' | 'desc'
Type SortRule {
field: String
direction (default: 'asc'): SortDirection
}
Type TableSettings {
default_sort_direction: SortDirection
// ...
}You've already seen aliases in the List and Dictionary examples above: Type ListInt = List[Int] works the same way.
Nested types
A field's type can be another custom type. This is handy when a group of fields belongs together and you want to reuse it:
Type Threshold {
value: Number
color: String
}
Type KpiSettings {
title: String
warning: Threshold
critical: Threshold
}
KpiSettings churn_settings {
title: 'Churn rate'
warning: Threshold { value: 0.05, color: '#F59E0B' }
critical: Threshold { value: 0.1, color: '#DC2626' }
}
churn_settings.critical.color // '#DC2626'If a group of fields is only used in one place, you can write it inline instead of declaring a separate type:
Type KpiSettings {
title: String
layout { // an inline type (`layout: {` also works here)
width: Number
height (default: 200): Number
}
}
KpiSettings revenue_settings {
title: 'Revenue'
layout { // no `:` here: `layout: {` would create a Dict instead
width: 400
}
}
revenue_settings.layout.height // 200Use custom types with functions
Custom types are most useful as function parameters, as shown at the start of this section. They also work as return types and as list elements:
// Return a custom type
Func value_segment(column: String, value: String) => SegmentSettings {
SegmentSettings {
label: 'Is ${value}'
column: column
value: value
}
}
// Accept a list of custom-type values
Func first_label(segments: List[SegmentSettings]) => String {
segments(0).label
}For a step-by-step guide with more examples, see Create your own types.
Advanced: field rules
(optional) and (default: ...) are field rules: options in parentheses after a field name that change how the field behaves. AML has several more, and they let a field accept repeated blocks. They're how AML's own built-in types are defined. Here's a simplified version of the built-in TableModel type:
Type TableModel (elem_name_as: 'name') {
table_name: String
label (optional): String
dimension (
elem_name_as: 'name',
multiple: true,
unique_elem_name,
root_namespace
): Dimension
// ...
}Those rules are why you can write any number of dimension id { ... } blocks in a model, and why users.id works as well as users.dimension.id. You can use the same rules in your own types. Most custom types don't need them, so reach for them when you want your type to read like a built-in one.
| Rule | What it does | Used with |
|---|---|---|
optional | The field can be left out | Any field |
default: <value> | The field can be left out, and gets this value | Any field |
multiple | The field can appear many times. Blocks are stored as a list | |
elem_name_as: '<key>' | Each block has a name (threshold warning { }), stored under <key> | Usually multiple |
unique_elem_name | Names must be unique. Blocks are stored by name | elem_name_as, multiple |
root_namespace | Named blocks can also be read directly from the parent value | elem_name_as, unique_elem_name |
allow_no_body | Blocks can leave out { } (model orders) | elem_name_as |
elem_type_as: '<key>' | Blocks start with your own keyword (not_null user_id { }), stored under <key> | One field per type |
unique_elem_type | Each keyword can be used once. Blocks are stored by keyword | elem_type_as |
Separate rules with commas, like (elem_name_as: 'name', multiple, unique_elem_name). A rule without a value, like multiple, is short for multiple: true.
A field that accepts repeated blocks is always optional: a value can leave it out entirely.
Stored names can't be read yet
The names and keywords these rules store (name and kind in the examples below) are part of the value, but AML code can't read them yet. For example, churn_settings.threshold.warning.name is an error. Use the name or keyword to look up a block instead. The exception is elem_name_as on a type, which you can read.
Repeated blocks: multiple
With multiple, a field can appear as many times as you need. The blocks are stored as a list, in the order you wrote them:
Type KpiSettings {
title: String
comparison (multiple) {
label: String
offset: Int
}
}
KpiSettings revenue_settings {
title: 'Revenue'
comparison { label: 'vs last month', offset: -1 }
comparison { label: 'vs last year', offset: -12 }
}
revenue_settings.comparison(1).label // 'vs last year'Named blocks: elem_name_as and unique_elem_name
Lists are fine until you need a specific block. To find blocks by name, give each one a name right after the field name, the way you write dimension id { }. elem_name_as tells AML which key to store the name under, and unique_elem_name stores the blocks by that name:
Type KpiSettings {
title: String
threshold (elem_name_as: 'name', multiple, unique_elem_name) {
value: Number
color: String
}
}
KpiSettings churn_settings {
title: 'Churn rate'
threshold warning { value: 0.05, color: '#F59E0B' }
threshold critical { value: 0.1, color: '#DC2626' }
}
churn_settings.threshold.critical.color // '#DC2626'With unique_elem_name, using the same name twice is an error (Duplicated name 'warning'.). Without it, names can repeat, and the blocks are stored as a list like with multiple alone (churn_settings.threshold(0)).
Read named blocks directly: root_namespace
Add root_namespace to skip the field name when reading a named block. This is what lets you write users.id instead of users.dimension.id. Both forms below work:
Type KpiSettings {
title: String
threshold (elem_name_as: 'name', multiple, unique_elem_name, root_namespace) {
value: Number
color: String
}
}
KpiSettings churn_settings {
title: 'Churn rate'
threshold critical { value: 0.1, color: '#DC2626' }
}
churn_settings.critical.color // '#DC2626'
churn_settings.threshold.critical.color // '#DC2626'root_namespace needs unique_elem_name, since names have to be unique to be looked up.
Blocks without a body: allow_no_body
Sometimes the name is all you need, like model users inside a dataset's view. allow_no_body lets a named block leave out its { }:
Type Report {
section (elem_name_as: 'name', multiple, unique_elem_name, allow_no_body) {
label (optional): String
}
}
Report sales_report {
section revenue // no body needed
section customers { label: 'Top customers' }
}
sales_report.section.customers.label // 'Top customers'If the blocks never need a body, leave { } out of the type as well:
Type ReportSettings {
title: String
hidden_field (elem_name_as: 'name', multiple, unique_elem_name, allow_no_body)
}
ReportSettings sales_report {
title: 'Sales'
hidden_field customer_email
hidden_field phone_number
}Your own keywords: elem_type_as and unique_elem_type
elem_type_as lets blocks start with a keyword you choose, instead of the field name. In not_null user_id { } below, not_null is the keyword (stored under kind) and user_id is the name (stored under column):
Type ColumnChecks {
table: String
check (elem_type_as: 'kind', elem_name_as: 'column', unique_elem_name) {
severity (default: 'error'): String
}
}
ColumnChecks orders_checks {
table: 'orders'
not_null user_id {}
unique order_id { severity: 'warn' }
}
orders_checks.check.order_id.severity // 'warn'Any line that doesn't match another field (like table) is checked against the elem_type_as field, so the keyword can be any word. For the same reason, only one field per type can use elem_type_as.
With unique_elem_type, blocks are stored by keyword instead, and each keyword can be used only once:
Type ColumnChecks {
check (elem_type_as: 'kind', unique_elem_type) {
columns: List[String]
}
}
ColumnChecks orders_checks {
not_null { columns: ['user_id', 'order_id'] }
unique { columns: ['order_id'] }
}
orders_checks.check.not_null.columns(1) // 'order_id'Name the value itself: elem_name_as on a type
elem_name_as also works on the type itself. Then a value of that type stores the name it's declared with, and unlike the field rules above, you can read it:
Type KpiSettings (elem_name_as: 'id') {
title: String
}
KpiSettings revenue_kpi {
title: 'Revenue'
}
revenue_kpi.id // 'revenue_kpi'Built-in types like TableModel and Dataset use this to store each object's name. elem_name_as is the only rule allowed on a type.
See also
AML Metric
Reference for the AML Metric object — an aggregated business measure that can be defined inside a dataset or as a standalone, reusable definition.
Date Format
How the format property works on date dimensions and measures in AML, with the day, month, year, and separator tokens and the patterns each one accepts.