AMQL Docs
AML Reference

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 = -10

Int

  • Represents numbers without decimal
Int a = 1
Int b = 2
Int c = -100

Boolean

Boolean is_truthy = true
Boolean is_falsy = false

String

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] (where T is 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] where K is a key’s type, V is 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 use snake_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 // 200

Use 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.

RuleWhat it doesUsed with
optionalThe field can be left outAny field
default: <value>The field can be left out, and gets this valueAny field
multipleThe 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_nameNames must be unique. Blocks are stored by nameelem_name_as, multiple
root_namespaceNamed blocks can also be read directly from the parent valueelem_name_as, unique_elem_name
allow_no_bodyBlocks 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_typeEach keyword can be used once. Blocks are stored by keywordelem_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

On this page