6. A vault of documents¶
The shelf of chapter 5 was a list written by hand. Here the same books are documents in a folder, and HQL reads the folder.
A vault is a folder¶
A vault is a directory of Markdown and HyperMarkDown documents. The tutorial vault looks like this:
examples/tutorial/
├── authors/
│ ├── frank-herbert.hmd
│ ├── jrr-tolkien.hmd
│ ├── mary-shelley.hmd
│ ├── virginia-woolf.hmd
│ └── walt-whitman.hmd
├── books/
│ ├── a-room-of-ones-own.hmd
│ ├── dune.hmd
│ ├── frankenstein.hmd
│ ├── leaves-of-grass.hmd
│ ├── mrs-dalloway.hmd
│ ├── the-common-reader.hmd
│ └── the-hobbit.hmd
├── club.hmd
├── frankenstein-inspires-dune.hmd
└── hql.toml
Each document is a card. A card has a header of metadata and a body someone wrote:
---
title: Dune
metadata:
category: book
author: Frank Herbert
year: 1965
genre: novel
pages: 412
status: reading
---
# Dune
A novel by [[frank-herbert]]. A desert planet, a spice, and a family that inherits both.
A program run inside a vault runs against it: hql takes the nearest
directory at or above the current one that holds .hmd/ or hql.toml.
--vault <directory> names one instead:
hql --vault examples/tutorial eval 'cards | size'
cards | size
14
cards¶
cards is every card of the vault, as a sequence. Its order is the order a
sorted walk of the folder reaches each document — the entries of each
directory by name, depth first — so a program answers the same on every
machine:
cards | take(4) | map(c => c.name)
["frank-herbert", "jrr-tolkien", "mary-shelley", "virginia-woolf"]
A card has the fields of a document: its name (the file stem), title,
path, format, body, its kind, and metadata, the header's tree.
A link is an optional card¶
@[[name]] refers to a card by name: @ followed by the link a document
writes, [[name]]. A link
may point at a document nobody has written yet, so its type is Option<Card>,
and reading a field through it reads through the option:
@[[dune]].title
"Dune"
@[[nowhere]].title
none
Metadata is Data¶
A card's metadata is the Data of chapter 2, so every question
from chapters 2 and 5 applies. The books are the cards whose category says so:
cards
| filter(c => c.metadata.category == "book")
| map(c => c.title)
["A Room of One's Own", "Dune", "Frankenstein", "Leaves of Grass", "Mrs Dalloway", "The Common Reader", "The Hobbit"]
The reading list¶
The club's reading list is the books still to read, by title:
cards
| filter(c => c.metadata.status == "todo")
| sort(by = c => c.title)
| map(c => c.title)
["Leaves of Grass", "The Common Reader", "The Hobbit"]
Read it as three steps, each consuming the one before. filter decides each
card on its own; sort needs every card before it can place the first; map
turns each card into its title. A step that decides each value alone may run
on any part of the input in any order, and a step that needs the whole input
is where the parts meet. That is the shape of MapReduce, and HQL keeps it
visible in the pipeline: nothing is stored in between, and nothing is a loop.
No one maintains this list. Mark a book status: done in its header and the
next run of the query no longer lists it.
Grouping cards¶
group partitions the books by a header value, as it partitioned the shelf:
cards
| filter(c => c.metadata.category == "book")
| group(by = c => c.metadata.status)
| map(g => {status: g.key, books: g.items | map(c => c.title)})
{{books: ["A Room of One's Own", "Frankenstein"], status: "done"}, {books: ["Dune", "Mrs Dalloway"], status: "reading"}, {books: ["Leaves of Grass", "The Common Reader", "The Hobbit"], status: "todo"}}
A typed view of a card works as the views of chapter 5 did, and the pages per author are a map over each group and a sum — map, then reduce:
fn pages(c: Card) : Int = match c.metadata.pages { some(Int n) => n, _ => 0 }
cards
| filter(c => c.metadata.category == "book")
| group(by = c => c.metadata.author)
| map(g => {author: g.key, pages: g.items | map(pages) | sum})
{{author: "Virginia Woolf", pages: 610}, {author: "Frank Herbert", pages: 412}, {author: "Mary Shelley", pages: 280}, {author: "Walt Whitman", pages: 145}, {author: "J. R. R. Tolkien", pages: 310}}
Kinds of card¶
A card whose header declares a relation between two other cards is a
RelationCard; every other card is a ConceptCard. of_type keeps the cards
of one kind:
cards | of_type(RelationCard) | map(c => c.title)
["Frankenstein inspires Dune"]
Chapter 7 is about what that relation means: cards and the links between them form a graph.
The vault itself¶
cards is a binding the standard library makes, cards = vault.cards, over
vault, the value the host seeds when it is given a vault:
[vault.name, vault.cards | size | text]
["tutorial", 14]
Outside every vault and without --vault there is no vault, and naming one
says so:
cards | size
name error: `cards` needs a vault: run inside one or pass --vault <directory>
--> <expression>:1:1
|
1 | cards | size
| ^^^^^