`map` and `array`, collections
map and array are how a rule works with collections. They exist because the rule language
cannot index or iterate one: there is no [0], no for, no comprehension. Every access goes
through a function call, and every one of these returns a default rather than raising.
That constraint shapes what rules look like. A rule does not walk a table; it asks a question about
it, find the row whose type is “principal”, how many rows are there, does this list contain
the code. Where you genuinely need per-row logic, the answer is usually a table read
(Rows) or a computation done before the rules run, not a loop.
Two rules hold for every function here:
- Pass collections straight to the call. A local variable cannot hold a map or a list: the
load-time check rejects
m = in.Map("x"), and one that slips through aborts the execution. - Parameter types are exact.
map.Get*,map.Hasand the other plainmapreads take a map, andarray.*takes the list typeto.Listproduces. A value typed as “anything”, such as whatarray.Firstreturns, must go throughto.Mapfirst; a list fromstrings.Split,in.Rowsorvocab.Itemsthroughto.List. Themap.Path*family takes any value and needs neither.
map.New() → map
Section titled “map.New() → map”Returns a fresh empty map.
map.Of(kv...) → map
Section titled “map.Of(kv...) → map”Builds a map from alternating key/value pairs. Each call returns a new map.
| Parameter | Type | Meaning |
|---|---|---|
kv | any, variadic | key1, value1, key2, value2, … |
rule RecordAffordability "keep the inputs to the affordability decision together" { when in.Double("loan.income", 0.0) > 0.0 then out.Set("affordability", map.Of("amount", in.Double("loan.amount", 0.0), "income", in.Double("loan.income", 0.0), "ratio", math.Round(in.Double("loan.amount", 0.0) / in.Double("loan.income", 0.0), 2)));}{ "loan": { "amount": 300000.0, "income": 90000.0 } }{ "affordability": { "amount": 300000.0, "income": 90000.0, "ratio": 3.33 } }An odd trailing argument is dropped without a word, so count them. A non-text key is converted to text. A list inside a map stays a list in the output, which is the way to return a list of strings.
map.Get*: reading one key with a default
Section titled “map.Get*: reading one key with a default”| Function | Returns |
|---|---|
map.GetStr(m, key, defaultVal) | string. The default unless the value is text: a number is not converted |
map.GetInt(m, key, defaultVal) | int64, from an integer or a float |
map.GetFloat(m, key, defaultVal) | float64, from a float or an integer |
map.GetBool(m, key, defaultVal) | bool. The default unless the value is a boolean |
map.GetMap(m, key) | map, or nil |
map.GetList(m, key) | list, or nil |
| Parameter | Type | Meaning |
|---|---|---|
m | map | The map to read |
key | string | The key |
defaultVal | matching type | Returned when the key is absent or holds another type |
rule ReadRowAmount "the amount column, defaulting to zero" { when array.Len(to.List(in.Rows("invoice.lines"))) > 0 then out.Set("firstLineAmount", map.GetFloat(to.Map(array.First(to.List(in.Rows("invoice.lines")))), "amount", 0.0));}{ "invoice": { "lines": [ { "sku": "A-1", "amount": 1200.0 }, { "sku": "B-7", "amount": 55.5 } ] } }{ "firstLineAmount": 1200.0 }Note the decimal point on 0.0, the same type-dispatch rule as elsewhere, and the to.Map around
array.First: without it the call aborts with Call using interface {} as type map[string]interface {}.
map.Set(m, key, value)
Section titled “map.Set(m, key, value)”Writes into a map in place. A nil map is ignored.
| Parameter | Type | Meaning |
|---|---|---|
m | map | The map to modify |
key | string | The key |
value | any | The value |
Since no variable can hold a map, map.Set only reaches a map you can name again, such as one
from in.Map. To build a map, use map.Of or map.SetPath, which return it. For results, use
out.Set: it records the writing rule and carries attributes.
map.Delete(m, key)
Section titled “map.Delete(m, key)”Removes a key, in place.
| Parameter | Type | Meaning |
|---|---|---|
m | map | The map to modify |
key | string | The key to remove |
map.Has(m, key) → bool
Section titled “map.Has(m, key) → bool”Reports whether the key is present.
| Parameter | Type | Meaning |
|---|---|---|
m | map | The map to test |
key | string | The key |
map.Keys(m) → list of string
Section titled “map.Keys(m) → list of string”Every key in the map, in no fixed order.
map.Len(m) → int64
Section titled “map.Len(m) → int64”How many entries.
map.IsEmpty(m) → bool
Section titled “map.IsEmpty(m) → bool”Whether the map has no entries. True for an empty map and for nil alike.
map.IsMap(v) → bool
Section titled “map.IsMap(v) → bool”Whether v is a map at all, for telling an envelope from a bare value before reading it.
Dotted paths into a map
Section titled “Dotted paths into a map”The Path* family reads a nested value in one call over a dotted path, where the Get* family
would need one call per level. Every hop is nil-safe, and each takes any value as m. A literal
dot in a key escapes as \. (written \\. inside a GRL string).
| Function | Returns |
|---|---|
map.Path(m, path) | Whatever is at path, or nil |
map.HasPath(m, path) | Whether path holds a non-nil value |
map.PathStr(m, path, def) | Text. A number at the path is rendered, not replaced by def |
map.PathNum(m, path, def) | float64. Document text such as "$412,000.00" converts |
map.PathInt(m, path, def) | int64, truncating |
map.PathBool(m, path, def) | bool. Yes, checked and the other affirmative words count |
map.PathMap(m, path) | The nested map, or an empty map, never nil |
map.PathRows(m, path) | Table rows, or an empty list |
rule BorrowerCity "the city from the nested address block" { when map.HasPath(in.Map("application"), "borrower.address.city") then out.Set("city", map.PathStr(in.Map("application"), "borrower.address.city", "")); out.Set("years", map.PathInt(in.Map("application"), "borrower.address.years", 0));}{ "application": { "borrower": { "address": { "city": "Austin", "years": 4 } } } }{ "city": "Austin", "years": 4 }For a fact, in.Str("application.borrower.address.city", "")
reads the same path directly. map.Path* is for a map that is not a fact: a row, a lookup result.
Shaping a map
Section titled “Shaping a map”Each returns a new map, so it can be passed straight to out.Set.
| Function | Returns |
|---|---|
map.SetPath(m, path, value) | m with value written at path, creating the levels on the way. A nil m starts a new map |
map.Merge(maps...) | The maps combined left to right; a later key wins. A nil argument is skipped |
map.Pick(m, keys...) | Only the named keys that are present. A dotted key reads a nested value and is stored under its last segment |
map.Omit(m, keys...) | Everything except the named top-level keys |
rule ShapeSummary "defaults under the caller's values, internal keys removed" { when in.Has("loan") then out.Set("summary", map.Omit(map.Merge(map.Of("currency", "USD", "term", 360), in.Map("loan")), "internalId"));}{ "loan": { "amount": 250000.0, "term": 180, "internalId": "x-77" } }{ "summary": { "amount": 250000.0, "currency": "USD", "term": 180 } }A list from to.List, array.New or another array call can be passed to any of these. Wrap any
other list in to.List first: array.Len(strings.Split(s, ",")) aborts with
Call using []string as type []interface {}, array.Len(to.List(strings.Split(s, ","))) works.
array.New() → list
Section titled “array.New() → list”A fresh empty list.
array.Len(arr) → int64
Section titled “array.Len(arr) → int64”How many elements.
| Parameter | Type | Meaning |
|---|---|---|
arr | list | The list |
Any list a call returns can also be counted with the language’s own .Len():
in.Rows("invoice.lines").Len(), bbox.Doc().Headings().Len(). Prefer .Len() for a list that is
not already one to.List built.
rule TooManyBorrowers "more than four borrowers needs manual review" { when array.Len(to.List(in.Rows("application.borrowers"))) > 4 then findings.Warn("TooManyBorrowers", "underwriting", "More borrowers than the automated path handles"); out.Set("route", "manual");}{ "application": { "borrowers": [ { "name": "A" }, { "name": "B" }, { "name": "C" }, { "name": "D" }, { "name": "E" }] } }{ "route": "manual" }array.Get(arr, index) → any
Section titled “array.Get(arr, index) → any”The element at index, zero-based, or nil when out of range.
| Parameter | Type | Meaning |
|---|---|---|
arr | list | The list |
index | int64 | Zero-based position |
array.GetStr(arr, index, defaultVal) → string · array.GetInt(...) → int64 · array.GetFloat(...) → float64
Section titled “array.GetStr(arr, index, defaultVal) → string · array.GetInt(...) → int64 · array.GetFloat(...) → float64”Typed access with a default, for an index that may not exist.
| Parameter | Type | Meaning |
|---|---|---|
arr | list | The list |
index | int64 | Zero-based position |
defaultVal | matching type | Returned when the index is out of range or the element is another type |
array.First(arr) → any · array.Last(arr) → any
Section titled “array.First(arr) → any · array.Last(arr) → any”The first or last element, or nil for an empty list.
| Parameter | Type | Meaning |
|---|---|---|
arr | list | The list |
Last is the one worth remembering: reaching the final element without knowing the length is
otherwise a two-step. Wrap the result in to.Map to read a row with map.Get*.
array.Contains(arr, value) → bool · array.ContainsStr(arr, value) → bool
Section titled “array.Contains(arr, value) → bool · array.ContainsStr(arr, value) → bool”Membership. ContainsStr takes a list of strings, which is what strings.Split, vocab.Items and
Headings() produce, without a to.List.
| Parameter | Type | Meaning |
|---|---|---|
arr | list | The list to search |
value | any / string | What to look for |
rule KnownFormType "the classified type is one we handle" { when !array.ContainsStr(vocab.Items("supported_forms"), out.Str("docType", "")) then findings.Error("KnownFormType", "classification", strings.Sprintf("Unsupported form type: %s", out.Str("docType", "")));}array.IndexOf(arr, value) → int64
Section titled “array.IndexOf(arr, value) → int64”The position of the first occurrence, or -1 when absent.
| Parameter | Type | Meaning |
|---|---|---|
arr | list | The list to search |
value | any | What to look for |
array.IsEmpty(arr) → bool
Section titled “array.IsEmpty(arr) → bool”Whether the list has no elements.
array.Append(arr, value) → list · array.Prepend(arr, value) → list
Section titled “array.Append(arr, value) → list · array.Prepend(arr, value) → list”A list with the element added at the end or the start.
| Parameter | Type | Meaning |
|---|---|---|
arr | list | The starting list |
value | any | What to add |
array.Reverse(arr) → list
Section titled “array.Reverse(arr) → list”A new list in reverse order.
The lists these return can be counted, searched or passed to another call. out.Set writes a list
of plain values as its text form ("[a b c]"); to return one as a list, put it inside a map.Of.
array.Slice(arr, start, end): not callable
Section titled “array.Slice(arr, start, end): not callable”Its bounds are a number type no rule value has, so every call aborts with
reflect: Call using int64 as type int. The load-time check does not catch it. Use array.Get,
array.First or array.Last for single elements.
Finding a row
Section titled “Finding a row”The two functions that make table work possible without iteration. Both take rows, a list of maps,
as in.Rows, to.Rows and Records() return them.
array.FindRowBy(rows, key, value) → map
Section titled “array.FindRowBy(rows, key, value) → map”Returns the first row whose key column contains value, ignoring case, or nil when none does.
| Parameter | Type | Meaning |
|---|---|---|
rows | list of map | The rows to search |
key | string | The column name |
value | string | The text the column must contain |
Containment rather than equality: "principal" finds a row whose type reads
"Principal & Interest". A nil result reads as absent through every map call, so no guard is
needed.
rule PrincipalPayment "the payment table has a principal row" { when map.Has(array.FindRowBy(in.Rows("payments"), "type", "principal"), "amount") then out.Set("principal", map.GetFloat(array.FindRowBy(in.Rows("payments"), "type", "principal"), "amount", 0.0));}{ "payments": [ { "type": "Escrow", "amount": 310.0 }, { "type": "Principal & Interest", "amount": 1842.5 }] }{ "principal": 1842.5 }On a document, pass the table’s rows: array.FindRowBy(bbox.Doc().Table().Records(), "type", "Principal").
array.FindRowByRegex(rows, key, pattern) → map
Section titled “array.FindRowByRegex(rows, key, pattern) → map”The same, matching the column against a regular expression. An invalid pattern finds nothing.
| Parameter | Type | Meaning |
|---|---|---|
rows | list of map | The rows to search |
key | string | The column name |
pattern | string | The regular expression to match |
The form to use when containment is too loose or the wording varies in ways a substring cannot
express, "Principal" but not "Principal Reduction".
rule PrincipalRowFlexible "match however this document words the principal row" { when map.Has( array.FindRowByRegex(bbox.Doc().Table().Records(), "type", "(?i)^principal( &| and) interest"), "amount") then out.Set("principal", map.GetFloat( array.FindRowByRegex(bbox.Doc().Table().Records(), "type", "(?i)^principal( &| and) interest"), "amount", 0.0));}Why there is no loop
Section titled “Why there is no loop”Deliberate: a rule states a condition and an action; a loop inside one is a program, and a program inside a rule is where rule engines stop being reviewable.
Where you find yourself wanting one:
| You want to | Do this instead |
|---|---|
| Act on each row | A Rows read, then hand Records() to out.Set |
| Find one row | array.FindRowBy / FindRowByRegex |
| Count rows | .Len() on the rows |
| Sum or aggregate a column | Not expressible in a rule. Compute it before the rules run and send it as a fact |
| Genuinely iterate with logic | A script, called before the rules run |
See also
Section titled “See also”- Tables and forms: where rows come from
to: conversion:to.Rows,to.List,to.Map,to.NumList- The rule language: what the grammar does and does not allow