Data: records and lists¶
The values JSON and YAML are made of — records of named fields, lists, and
scalar leaves — are one type in HQL, Data. This page does with Data what
jq does with JSON: build it, read into it, reshape it, and write it back out.
A tree of values¶
A data literal is written in braces, with a key, a colon and a value per field. Values may be scalars, lists, or other records:
{title: "Dune", author: {name: "Frank Herbert", born: 1920}, tags: ["desert", "spice"]}
{author: {born: 1920, name: "Frank Herbert"}, tags: ["desert", "spice"], title: "Dune"}
A key that is not an identifier is quoted: {"first name": "Ada"}. The record
prints with its keys in order, so the same tree always prints the same way.
The colon is what makes braces a record. The empty record is {:}; {}
without a colon is the empty set, as {1, 2} is a set of two. So
d : Data = {:} holds an empty record, and d : Data = {} is a type error:
{} is a Set<Never>, which a Set<Int> binding accepts and a Data one
does not.
Reading a field¶
A path reads into the tree, one key per step:
book = {title: "Dune", author: {name: "Frank Herbert", born: 1920}}
book.author.name
"Frank Herbert"
A key the tree does not have is not an error. The key belonged to whoever wrote the record, so nothing promised it, and reading it gives absence:
book = {title: "Dune"}
book.year
none
The type says so. Reading a key of a Data record gives an Option<Data>: a
some holding a tree, or none. HQL has no null; a value that may be missing
is an Option, and the program says what absence means where it matters.
year = {title: "Dune"}.year
match year {
some(_) => "dated",
none => "undated",
}
"undated"
Asking what a leaf is¶
A leaf of a tree could be any scalar; the record did not say. Equality compares
through that, so book.title == "Dune" is an ordinary question:
{title: "Dune"}.title == "Dune"
true
Arithmetic and ordering need to know the type. is(T) asks whether a value is
a T, and a match arm with a type, some(Int n), both tests and reads it:
pages = {title: "Dune", pages: 412}.pages
match pages {
some(Int n) => n * 2,
_ => 0,
}
824
Lists of records¶
A list of records is a List<Data>. map applies a function to every element,
and x => x.title is a function written in place, a lambda:
books = [{title: "Dune", pages: 412}, {title: "The Hobbit", pages: 310}, {title: "Leaves of Grass"}]
books | map(b => b.title)
["Dune", "The Hobbit", "Leaves of Grass"]
| is a pipe: the value on its left is the input of the step on its right.
filter keeps the elements a predicate accepts:
books = [{title: "Dune", pages: 412}, {title: "The Hobbit", pages: 310}, {title: "Leaves of Grass"}]
books
| filter(b => match b.pages { some(Int n) => n > 300, _ => false })
| map(b => b.title)
["Dune", "The Hobbit"]
To add up the page counts, the leaves become numbers first. somes drops the
absent ones, of_type(Int) keeps the leaves that are integers and reads each as
the Int it holds, and sum adds them:
books = [{title: "Dune", pages: 412}, {title: "The Hobbit", pages: 310}, {title: "Leaves of Grass"}]
books
| map(b => b.pages)
| somes
| of_type(Int)
| sum
722
Each step is checked against the type the step before it produced. After
of_type(Int) the list is a list of Int, which is what sum takes; without
it the checker refuses sum over trees, before anything runs.
Reshaping¶
A data literal can hold any expression, so map builds a new record from each
old one:
books = [{title: "Dune", author: "Frank Herbert"}, {title: "Emma", author: "Jane Austen"}]
books | map(b => {book: b.title, by: b.author})
[{book: "Dune", by: "Frank Herbert"}, {book: "Emma", by: "Jane Austen"}]
merge combines two records. Every key of either is kept, and a key in both
takes the value from the right:
merge({title: "Dune", pages: 412}, {pages: 896, edition: "illustrated"})
{edition: "illustrated", pages: 896, title: "Dune"}
Writing it back out¶
A value is written in a presentation format by a step named for it. The
result is text of that format — Json, Yaml, Csv, Markdown — and
hql writes it as it is:
[{title: "Dune", pages: 412}, {title: "The Hobbit", pages: 310}] | json
[
{
"pages": 412,
"title": "Dune"
},
{
"pages": 310,
"title": "The Hobbit"
}
]
{title: "Dune", pages: 412, tags: ["desert", "spice"]} | yaml
pages: 412
tags:
- desert
- spice
title: Dune
[{title: "Dune", pages: 412}, {title: "The Hobbit", pages: 310}] | csv
pages,title
412,Dune
310,The Hobbit
A list of records becomes a table when it is built into one. Table takes
the records as rows and columns names their fields in the order the table
writes them; each format then writes the table in its own syntax, here a pipe
table:
[{title: "Dune", pages: 412}, {title: "The Hobbit", pages: 310}]
| Table(columns = ["title", "pages"])
| markdown
| title | pages |
| ---------- | ----- |
| Dune | 412 |
| The Hobbit | 310 |
Chapter 10 is about formats in full.
A program writes its data as literals, reads it from a vault's documents, as
chapter 6 does, or reads a JSON, YAML or CSV file with
source, as chapter 13 does.
What is next¶
How Data differs from a set and from a map, all three written in braces, is
the next page. A Data list is one kind of
collection. Chapter 3 meets
the others — lists, sets and maps of typed values — and the steps that work on
all of them.