A language tourUm tour pela linguagem

Hive, one idea at a timeHive, uma ideia por vez

Hive is a small, statically-typed language built around tables, values that behave like values, and mistakes caught before the program runs. This tour walks its whole surface in 171 steps. Each one introduces exactly one thing.

How to read it. Straight down. Every step is short, every snippet is complete enough to try, and nothing is used before it has been introduced.

Run anything you see with hive run file.hive. Where a step shows code that is meant to be refused, it is marked as such — Hive rejects a lot at compile time, and knowing what it rejects is half of knowing the language.

Hive é uma linguagem pequena e de tipagem estática, construída em torno de tabelas, de valores que se comportam como valores, e de erros apanhados antes de o programa rodar. Este tour percorre toda a linguagem em 171 passos. Cada um apresenta exatamente uma coisa.

Como ler. De cima para baixo. Cada passo é curto, cada exemplo é completo o suficiente para você testar, e nada aparece antes de ter sido apresentado.

Rode qualquer exemplo com hive run arquivo.hive. Quando um passo mostra código que deve ser recusado, isso vem sinalizado — o Hive rejeita muita coisa em tempo de compilação, e saber o que ele rejeita é metade de conhecer a linguagem.

Part IParte I

Getting startedPrimeiros passos

What Hive is, how to run it, and the smallest program that does something.O que é o Hive, como rodá-lo, e o menor programa que faz alguma coisa.

1What Hive isO que é o Hive

Hive is a compiled language. You write .hive files; the compiler checks them and produces a native executable for your machine. There is no virtual machine to install alongside your program and no interpreter to ship.

Three ideas shape everything else in this tour:

  • Tables are a built-in idea. Reading a CSV, a spreadsheet or a SQL result is one keyword, and the thing you get back is an ordinary value.
  • Values behave like values. Binding a vector to a second name gives you a second value, not a second window onto the first.
  • The compiler would rather refuse than guess. Out-of-range indexes, unhandled branches and mistyped queries are compile errors, not runtime surprises.

Under the hood the compiler translates your program to Go and hands that to the Go toolchain, which is where the native binary and the lightweight threads come from. You do not write Go and you do not read it — but it is worth knowing, because it explains a few of the rules further down. Step 170 shows the mapping.

O Hive é uma linguagem compilada. Você escreve arquivos .hive; o compilador os confere e produz um executável nativo para a sua máquina. Não há máquina virtual para instalar junto do seu programa e nem interpretador para distribuir.

Três ideias moldam todo o resto deste tour:

  • Tabelas são uma ideia embutida. Ler um CSV, uma planilha ou um resultado de SQL é uma palavra-chave, e o que você recebe de volta é um valor comum.
  • Valores se comportam como valores. Vincular um vetor a um segundo nome dá a você um segundo valor, não uma segunda janela para o primeiro.
  • O compilador prefere recusar a adivinhar. Índices fora da faixa, ramos não tratados e queries mal tipadas são erros de compilação, não surpresas em execução.

Por baixo, o compilador traduz seu programa para Go e entrega isso à toolchain do Go, que é de onde vêm o binário nativo e as threads leves. Você não escreve Go e não lê Go — mas vale saber, porque isso explica algumas das regras mais adiante. O passo 170 mostra o mapeamento.

2The five commandsOs cinco comandos

The compiler is one CLI with five verbs. Each takes the file holding your main.

hive build app.hive   # compile to a native executable next to the file
hive run   app.hive   # compile and run it straight away
hive test  app.hive   # run the program's tests, with coverage
hive check app.hive   # report errors, build nothing
hive emit  app.hive   # print the generated Go, for when you are curious

hive check is the one you will use most while learning: it is the fastest way to ask "would this compile?". Errors come back as file:line: message, which is the shape editors already know how to jump to.

Anything after the file name is passed to your program rather than to the compiler, so hive run app.hive a b c gives your program three arguments.

O compilador é uma CLI com cinco verbos. Cada um recebe o arquivo que contém o seu main.

hive build app.hive   # compila para um executável nativo ao lado do arquivo
hive run   app.hive   # compila e roda na hora
hive test  app.hive   # roda os testes do programa, com cobertura
hive check app.hive   # reporta erros, não constrói nada
hive emit  app.hive   # imprime o Go gerado, para quando der curiosidade

O hive check é o que você mais vai usar enquanto aprende: é o jeito mais rápido de perguntar "isso compilaria?". Os erros voltam como arquivo:linha: mensagem, que é o formato que os editores já sabem seguir.

Qualquer coisa depois do nome do arquivo é passada ao seu programa, e não ao compilador, então hive run app.hive a b c dá três argumentos ao seu programa.

3Your first programSeu primeiro programa

A program starts at main. It is a proc — a procedure — and it returns void, meaning nothing.

proc main(): void {
	echo "Hello from the hive."
}
prints
Hello from the hive.

echo writes one line to the terminal. It is not a function you call with parentheses — it is a statement, and it takes any value, not just text. Give it a number, a vector, a whole table, and it renders that value and appends a newline.

proc main(): void {
	echo 42
	echo ["a", "b", "c"]
}
prints
42
[a b c]

Um programa começa no main. Ele é um proc — um procedimento — e retorna void, ou seja, nada.

proc main(): void {
	echo "Hello from the hive."
}
imprime
Hello from the hive.

O echo escreve uma linha no terminal. Ele não é uma função que você chama com parênteses — é um comando, e recebe qualquer valor, não só texto. Dê a ele um número, um vetor, uma tabela inteira, e ele renderiza aquele valor e adiciona uma quebra de linha.

proc main(): void {
	echo 42
	echo ["a", "b", "c"]
}
imprime
42
[a b c]

4Comments, and a word about caseComentários, e uma palavra sobre maiúsculas

Comments start with // and run to the end of the line. That is the only comment form.

// A comment on its own line.
proc main(): void {
	echo "hi"   // or at the end of one
}

Every keyword is lower case, and only lower case. PROC is not proc — and it is not a name of your own either, because a keyword is reserved however it is spelled.

Every other name has a shape too, and the shape says what the name is: camelCase for variables, parameters, fields and every proc, func and query; UPPER_CASE for a variable nothing ever reassigns, which is how a constant is written; PascalCase for types, their variants and atoms — User, Result.Ok, #Ready. The types the language declares are spelled no differently from yours, so Str is a type and str is not.

type Colony {          // PascalCase — a type
	name:   Str        // camelCase — a field
	frames: Int
}

func isBusy(colony: Colony): Bool {   // camelCase — a func, and its parameter
	MAX_FRAMES := 10                  // UPPER_CASE — nothing reassigns it
	return colony.frames >= MAX_FRAMES
}

These are rules, not habits: the compiler holds you to them, and a name that breaks one is told so with the spelling it should have had. The point is that nothing has to be looked up — you can see what a word is before you know what it means.

Convention

A name may open with a single __scratch, _helperOf, _MAX. The compiler asks nothing of the prefix and offers nothing for it: it is a note to whoever reads the name next, saying this one is private, or is here only because something had to be. On its own, _ is the binding that throws its value away.

Comentários começam com // e vão até o fim da linha. Essa é a única forma de comentário.

// Um comentário em linha própria.
proc main(): void {
	echo "hi"   // ou no fim de uma linha
}

Toda palavra-chave é minúscula, e só minúscula. PROC não é proc — e também não é um nome seu, porque uma palavra-chave fica reservada em qualquer grafia.

Todo outro nome também tem um formato, e o formato diz o que o nome é: camelCase para variáveis, parâmetros, campos e todo proc, func e query; UPPER_CASE para uma variável que nada reatribui, que é como se escreve uma constante; PascalCase para tipos, suas variantes e átomos — User, Result.Ok, #Ready. Os tipos que a linguagem declara se escrevem igual aos seus, então Str é um tipo e str não é.

type Colony {          // PascalCase — um tipo
	name:   Str        // camelCase — um campo
	frames: Int
}

func isBusy(colony: Colony): Bool {   // camelCase — um func, e seu parâmetro
	MAX_FRAMES := 10                  // UPPER_CASE — nada reatribui
	return colony.frames >= MAX_FRAMES
}

São regras, não hábitos: o compilador cobra cada uma, e um nome que quebre alguma é avisado com a grafia que deveria ter. A ideia é que nada precise ser consultado — dá para ver o que uma palavra é antes de saber o que ela significa.

Convenção

Um nome pode começar com um único __scratch, _helperOf, _MAX. O compilador não exige nada do prefixo e não oferece nada por ele: é um recado para quem ler o nome depois, dizendo que este é privado, ou que só está ali porque algo precisava estar. Sozinho, _ é a ligação que joga o valor fora.

Part IIParte II

The values you start withOs valores com que você começa

Text, numbers, booleans and atoms — the five scalar types, and the operators that come with them.Texto, números, booleanos e átomos — os cinco tipos escalares, e os operadores que vêm com eles.

5Text is StrTexto é Str

A string literal sits in double quotes. The type is called Str, and it is always UTF-8.

proc main(): void {
	Str greeting = "Good evening"
	echo greeting
}

That line reads: a Str called greeting, equal to "Good evening". The type comes first. Step 17 shows the shorter form that infers it.

Um literal de string fica entre aspas duplas. O tipo se chama Str, e é sempre UTF-8.

proc main(): void {
	Str greeting = "Good evening"
	echo greeting
}

Essa linha se lê: um Str chamado greeting, igual a "Good evening". O tipo vem primeiro. O passo 17 mostra a forma curta, que infere o tipo.

6Interpolation: "{expr}"Interpolação: "{expr}"

Braces inside a string are holes, and whatever expression you put in one is rendered into the string at that point.

proc main(): void {
	name := "Ada"
	visits := 3
	echo "Welcome back, {name} — visit number {visits}."
}
prints
Welcome back, Ada — visit number 3.

The hole takes an expression, not just a name, so "{len(names)} names" and "total: {a + b}" both work. Non-text values are rendered the way echo would render them.

Chaves dentro de uma string são buracos, e a expressão que você põe num deles é renderizada na string naquele ponto.

proc main(): void {
	name := "Ada"
	visits := 3
	echo "Welcome back, {name} — visit number {visits}."
}
imprime
Welcome back, Ada — visit number 3.

O buraco aceita uma expressão, não só um nome, então "{len(names)} names" e "total: {a + b}" funcionam. Valores que não são texto são renderizados do jeito que o echo os renderizaria.

7Multiline stringsStrings de várias linhas

Backticks open a string that may span lines. The indentation you used to keep the source tidy is stripped at compile time, so the value holds the text and not the leading tabs.

proc main(): void {
	usage := `
		hive build <file>
		hive run   <file>
	`
	echo usage
}
prints
hive build <file>
hive run   <file>

Acentos graves abrem uma string que pode ocupar várias linhas. A indentação que você usou para deixar o código arrumado é removida em tempo de compilação, então o valor guarda o texto e não as tabulações da frente.

proc main(): void {
	usage := `
		hive build <file>
		hive run   <file>
	`
	echo usage
}
imprime
hive build <file>
hive run   <file>

8Joining text with +Juntando texto com +

+ between two strings concatenates them.

proc main(): void {
	first := "Grace"
	last := "Hopper"
	echo first + " " + last
}

Interpolation and + do the same job; pick whichever reads better. A long sentence with two values in the middle is usually clearer interpolated, and gluing two names together is usually clearer with +.

O + entre duas strings concatena as duas.

proc main(): void {
	first := "Grace"
	last := "Hopper"
	echo first + " " + last
}

Interpolação e + fazem o mesmo trabalho; escolha o que ler melhor. Uma frase longa com dois valores no meio geralmente fica mais clara interpolada, e colar dois nomes geralmente fica mais claro com +.

9Measuring text: len and bytesMedindo texto: len e bytes

Two built-in functions measure a string, and they answer different questions. len counts characters. bytes counts the bytes of its UTF-8 encoding.

proc main(): void {
	word := "café"
	echo len(word)     // 4 — four characters
	echo bytes(word)   // 5 — the é takes two bytes
}

For plain ASCII the two agree, which is exactly why it is worth knowing they are different functions before you meet a string that is not.

Duas funções embutidas medem uma string, e elas respondem perguntas diferentes. O len conta caracteres. O bytes conta os bytes da codificação UTF-8.

proc main(): void {
	word := "café"
	echo len(word)     // 4 — quatro caracteres
	echo bytes(word)   // 5 — o é ocupa dois bytes
}

Para ASCII puro as duas concordam, e é justamente por isso que vale saber que são funções diferentes antes de você encontrar uma string que não é ASCII.

10A Str has no indexUm Str não tem índice

This is the first rule Hive will refuse you on, and the reason is worth a moment.

does not compile
initial := name[0]
prefix  := name[0:3]

Subscripting addresses bytes, while a Str is a sequence of characters — which is what len counts. The two only line up for ASCII, and a single byte taken from the middle of a character is not text at all. Rather than be right most of the time, Hive rejects the operation.

Take a string apart with split, search it with indexOf, or match it against a template with a string pattern. All three arrive later in the tour; for now, just know that s[0] is not the tool.

Esta é a primeira regra em que o Hive vai recusar você, e o motivo merece um instante.

não compila
initial := name[0]
prefix  := name[0:3]

Indexar endereça bytes, enquanto um Str é uma sequência de caracteres — que é o que o len conta. Os dois só coincidem em ASCII, e um único byte tirado do meio de um caractere não é texto nenhum. Em vez de estar certo na maior parte das vezes, o Hive rejeita a operação.

Desmonte uma string com split, busque nela com indexOf, ou case-a com um gabarito usando um padrão de string. As três chegam mais adiante no tour; por ora, basta saber que s[0] não é a ferramenta.

11Numbers: Int and FloatNúmeros: Int e Float

There are two number types. Int is a 64-bit signed integer; Float is a 64-bit floating point number. A literal with a decimal point is a Float, one without is an Int.

proc main(): void {
	Int   frames = 10
	Float weight = 2.5
	echo frames
	echo weight
}

They are separate types and do not silently mix. To move between them, ask hive.conv — step 116 — which widens an Int to a Float and rounds a Float back down to an Int.

Existem dois tipos numéricos. Int é um inteiro de 64 bits com sinal; Float é um ponto flutuante de 64 bits. Um literal com ponto decimal é um Float, um sem ponto é um Int.

proc main(): void {
	Int   frames = 10
	Float weight = 2.5
	echo frames
	echo weight
}

São tipos separados e não se misturam silenciosamente. Para transitar entre eles, peça ao hive.conv — passo 116 — que alarga um Int para Float e arredonda um Float de volta para Int.

12The arithmetic operatorsOs operadores aritméticos

Six of them: + - * / % **.

proc main(): void {
	echo 7 + 2      // 9
	echo 7 - 2      // 5
	echo 7 * 2      // 14
	echo 7 / 2      // 3   — Int division truncates
	echo 7 % 2      // 1   — the remainder
	echo 7 ** 2     // 49  — seven squared
}

% is the remainder, and it binds as tightly as * and /. ** is exponentiation. Prefix - negates, and sits between the two in precedence: it binds tighter than * but looser than **, so -2 ** 2 means -(2 ** 2), while 2 ** -3 reads the minus sign as part of the exponent.

Comparison gives you == != < > <= >=, and && / || combine conditions.

São seis: + - * / % **.

proc main(): void {
	echo 7 + 2      // 9
	echo 7 - 2      // 5
	echo 7 * 2      // 14
	echo 7 / 2      // 3   — divisão de Int trunca
	echo 7 % 2      // 1   — o resto
	echo 7 ** 2     // 49  — sete ao quadrado
}

O % é o resto, e liga tão firme quanto * e /. O ** é exponenciação. O - prefixado nega, e fica entre os dois em precedência: liga mais firme que * mas menos que **, então -2 ** 2 quer dizer -(2 ** 2), enquanto 2 ** -3 lê o sinal de menos como parte do expoente.

Para comparação você tem == != < > <= >=, e && / || combinam condições.

13Stepping a number: += and ++Andando um número: += e ++

A number you are allowed to change (step 19) can be updated in place. Each of these is shorthand for the assignment it looks like.

proc main(): void {
	mut total := 0
	total += 5      // total = total + 5
	total -= 1      // total = total - 1
	total *= 3      // total = total * 3
	total /= 2      // total = total / 2
	total++         // total = total + 1
	total--         // total = total - 1
	echo total
}
prints
6

+= also works on a Str, where it appends.

Um número que você tem permissão de mudar (passo 19) pode ser atualizado no lugar. Cada um destes é atalho para a atribuição com a qual se parece.

proc main(): void {
	mut total := 0
	total += 5      // total = total + 5
	total -= 1      // total = total - 1
	total *= 3      // total = total * 3
	total /= 2      // total = total / 2
	total++         // total = total + 1
	total--         // total = total - 1
	echo total
}
imprime
6

O += também funciona num Str, onde ele acrescenta ao fim.

14Arithmetic at the edgesAritmética nas bordas

Most arithmetic is unsurprising. These cases are the ones worth stating outright, so that nothing is left to chance.

expressionresult
a / 0, a % 00 — dividing by zero is a value, not a crash
Int overflowwraps, silently, two's-complement
2 ** 1000 — that same wrap, reached by multiplying
n ** k, k < 00 — including 1 ** -1
n ** 01
-7 % 3-1 — the sign follows the dividend
10.0 ** 400.0+InfFloat does go non-finite

A negative Int exponent has no integral answer, so it yields 0 rather than a fraction. If you want the mathematical answer, work in Float, where ** is real exponentiation.

One deliberate gap

Converting a Float that is +Inf, -Inf, NaN or simply enormous into an Int is unspecified. The value you get may differ between machines. Non-finite floats reach a program through overflow and through parsing "Inf", so check the range you expect before converting if it matters.

A maior parte da aritmética não tem surpresa. Estes casos são os que valem ser ditos em voz alta, para que nada fique por acaso.

expressãoresultado
a / 0, a % 00 — dividir por zero é um valor, não um crash
estouro de Intdá a volta, silenciosamente, em complemento de dois
2 ** 1000 — a mesma volta, alcançada multiplicando
n ** k, k < 00 — incluindo 1 ** -1
n ** 01
-7 % 3-1 — o sinal segue o dividendo
10.0 ** 400.0+InfFloat realmente vai a valores não finitos

Um expoente Int negativo não tem resposta inteira, então ele entrega 0 em vez de uma fração. Se você quer a resposta matemática, trabalhe em Float, onde ** é exponenciação real.

Uma lacuna deliberada

Converter um Float que é +Inf, -Inf, NaN ou simplesmente enorme para Int é não especificado. O valor que você recebe pode diferir entre máquinas. Floats não finitos chegam a um programa por estouro e por conversão de "Inf", então confira a faixa que você espera antes de converter, se isso importa.

15Bool is a real booleanBool é um booleano de verdade

Two literals, true and false. Comparisons produce a Bool, and so do && and ||.

proc main(): void {
	Bool ready = true
	waiting := false
	echo ready || waiting     // true
	echo ready && waiting     // false
	echo 3 > 2                // true
}

Dois literais, true e false. Comparações produzem um Bool, e && e || também.

proc main(): void {
	Bool ready = true
	waiting := false
	echo ready || waiting     // true
	echo ready && waiting     // false
	echo 3 > 2                // true
}

16Atoms: names that are valuesÁtomos: nomes que são valores

An atom is written with a leading # and no space: #Ok, #Pending, #SomeAtom. It is a symbol whose whole content is its own name — useful when you want a fixed set of labels and nothing more.

proc main(): void {
	Atom state = #Pending
	echo state
}
prints
Pending

The compiler collects every atom in your program into a table and gives each one a small integer. That table is embedded in the executable, which is what lets echo print the name rather than the number. Coercing an atom into a Str gives you its number instead:

assert "0" + #Nil == "00"

One atom comes with the language: #Nil, and it is always first in the table, so its number is always 0. Everything else is yours — an atom exists because your program mentioned it, and it lands wherever its first mention puts it.

What an atom is not is a condition. It is a label, not a yes or a no, so a bare atom where a Bool belongs is a compile error:

does not compile
flag := #Ready
if flag {
	echo "which atom did that mean?"
}

Compare it with the one you mean instead — if flag == #Ready — which says the same thing in one more operator and cannot be read two ways.

Because atoms cannot be computed — you cannot build one from a string at runtime — the compiler always knows the complete set, which matters later when services get named (step 139).

Um átomo é escrito com um # na frente e sem espaço: #Ok, #Pending, #SomeAtom. É um símbolo cujo conteúdo é o próprio nome — útil quando você quer um conjunto fixo de rótulos e nada além disso.

proc main(): void {
	Atom state = #Pending
	echo state
}
imprime
Pending

O compilador reúne todo átomo do seu programa numa tabela e dá a cada um um inteiro pequeno. Essa tabela é embutida no executável, que é o que permite ao echo imprimir o nome em vez do número. Coagir um átomo para Str entrega o número:

assert "0" + #Nil == "00"

Um átomo vem com a linguagem: #Nil, e ele é sempre o primeiro da tabela, então o número dele é sempre 0. Todo o resto é seu — um átomo existe porque o seu programa o mencionou, e ele cai onde a primeira menção o colocar.

O que um átomo não é: uma condição. Ele é um rótulo, não um sim ou um não, então um átomo sozinho onde cabe um Bool é erro de compilação:

não compila
flag := #Ready
if flag {
	echo "which atom did that mean?"
}

Compare com o que você quer dizer — if flag == #Ready — que diz a mesma coisa com um operador a mais e não pode ser lido de dois jeitos.

Como átomos não podem ser calculados — você não pode construir um a partir de uma string em tempo de execução — o compilador sempre conhece o conjunto completo, o que importa mais adiante, quando serviços recebem nomes (passo 139).

Part IIIParte III

Names and mutabilityNomes e mutabilidade

How a value gets a name, and why changing one is something you have to ask for.Como um valor ganha um nome, e por que mudar um é algo que você precisa pedir.

17Two ways to declare a nameDois jeitos de declarar um nome

You can write the type out, or let the compiler read it off the value.

Str  title = "Keeper's log"     // spelled out
name := "Ada"                   // inferred: a Str, obviously

:= is the short form and the one you will reach for most. Write the type when it adds something — when the value is a literal whose type you want to pin down, or when the declaration is documentation for whoever reads it next.

Both forms produce the same thing. There is no runtime difference at all.

Você pode escrever o tipo, ou deixar o compilador lê-lo a partir do valor.

Str  title = "Keeper's log"     // escrito
name := "Ada"                   // inferido: um Str, claramente

:= é a forma curta e a que você mais vai usar. Escreva o tipo quando isso acrescenta algo — quando o valor é um literal cujo tipo você quer fixar, ou quando a declaração serve de documentação para quem ler depois.

As duas formas produzem a mesma coisa. Não há diferença nenhuma em tempo de execução.

18Names are immutable by defaultNomes são imutáveis por padrão

A name, once bound, stays bound. Assigning to it again is a compile error.

does not compile
name := "Ada"
name = "Grace"

Nothing about that line is wrong as an idea — you simply have to say up front that the name is one you intend to change.

This is the default because most names never need to change, and a name that cannot change is one fewer thing to keep track of while reading. It also underpins Part V, where immutability is what lets the compiler skip copying data.

Um nome, uma vez vinculado, permanece vinculado. Atribuir a ele de novo é erro de compilação.

não compila
name := "Ada"
name = "Grace"

Não há nada de errado com essa linha como ideia — você só precisa dizer de antemão que aquele nome é um que você pretende mudar.

Este é o padrão porque a maioria dos nomes nunca precisa mudar, e um nome que não pode mudar é uma coisa menos para acompanhar durante a leitura. Também é a base da Parte V, onde a imutabilidade é o que permite ao compilador evitar copiar dados.

19mut: asking for a name you can changemut: pedindo um nome que você pode mudar

Prefix the declaration with mut and reassignment is allowed.

proc main(): void {
	mut name := "Ada"
	name = "Grace"
	echo name

	mut Int count = 0
	count += 1
	echo count
}
prints
Grace
1

mut works with both declaration forms: mut x := … and mut T x = …. It buys you three things — reassignment, writing through an index (v[0] = …) or a field (b.items = …), and growing a vector with append. Everything in the rest of the tour that mutates something needs it.

Prefixe a declaração com mut e a reatribuição é permitida.

proc main(): void {
	mut name := "Ada"
	name = "Grace"
	echo name

	mut Int count = 0
	count += 1
	echo count
}
imprime
Grace
1

O mut funciona com as duas formas de declaração: mut x := … e mut T x = …. Ele compra três coisas — reatribuição, escrita por índice (v[0] = …) ou por campo (b.items = …), e crescimento de vetor com append. Tudo no resto do tour que muta algo precisa dele.

20A mut T is conceptually a Mutex<T>Um mut T é, conceitualmente, um Mutex<T>

This is the mental model the language is built on, and it explains behaviour further down, so it is worth taking on now.

A mutable value is not "a T with a flag". Think of it as a Mutex<T>: at runtime it is indistinguishable from a plain T, but at compile time it is a different thing, and only a mutex may be altered.

From that one idea follows the direction of travel: a slot that wants a T happily accepts a Mutex<T>, because a mutex is a T plus a permission the slot does not intend to use. The reverse never holds.

func describe(text: Str): Str {     // wants a plain Str
	return "[" + text + "]"
}

proc main(): void {
	mut label := "frames"            // a Mutex<Str>
	echo describe(label)             // fine: the func just sees a Str
}

And so assigning to a parameter, or to a plain := binding, is a compile error: neither is a mutex.

Este é o modelo mental sobre o qual a linguagem é construída, e ele explica comportamentos mais adiante, então vale absorver agora.

Um valor mutável não é "um T com uma bandeirinha". Pense nele como um Mutex<T>: em tempo de execução ele é indistinguível de um T comum, mas em tempo de compilação é outra coisa, e só mutexes podem ser alterados.

Dessa única ideia sai a direção do fluxo: um slot que quer um T aceita de bom grado um Mutex<T>, porque um mutex é um T mais uma permissão que o slot não pretende usar. O contrário nunca vale.

func describe(text: Str): Str {     // quer um Str comum
	return "[" + text + "]"
}

proc main(): void {
	mut label := "frames"            // um Mutex<Str>
	echo describe(label)             // ok: a func só vê um Str
}

E é por isso que atribuir a um parâmetro, ou a um vínculo feito com :=, é erro de compilação: nenhum dos dois é um mutex.

21A mutex handed to a callable is copied inUm mutex entregue a um callable é copiado na entrada

If a func or proc receives a mutable value in an ordinary parameter, the callee sees an immutable T — and Hive makes that view honest by copying the value on the way in.

func report(frames: Int[dyn]): Str {
	return "counted {len(frames)} supers"
}

proc main(): void {
	mut Int[dyn] frames = [10, 9, 11]
	counted := await [report(frames)]  // frames is copied in, here and now
	append(frames, 12)                 // the callee never sees this
	echo counted[0]
}
prints
counted 3 supers

The callee's Int[dyn] would not really be immutable if the caller could keep writing to it. Copying at the boundary is what makes the promise true — and, as the example shows, it holds whether the two run one after the other or at the same time.

Keep in mind

This is the one case where a copy is unconditional. Everywhere else, Hive copies only when it must — which is the subject of Part V. A proc can also ask for the mutex rather than a copy of it, which is the next step.

Se uma func ou proc recebe um valor mutável em um parâmetro comum, quem foi chamado vê um T imutável — e o Hive torna essa visão honesta copiando o valor na entrada.

func report(frames: Int[dyn]): Str {
	return "counted {len(frames)} supers"
}

proc main(): void {
	mut Int[dyn] frames = [10, 9, 11]
	counted := await [report(frames)]  // frames é copiado na entrada, aqui e agora
	append(frames, 12)                 // quem foi chamado nunca vê isto
	echo counted[0]
}
imprime
counted 3 supers

O Int[dyn] de quem foi chamado não seria de fato imutável se quem chamou pudesse continuar escrevendo nele. Copiar na fronteira é o que torna a promessa verdadeira — e, como o exemplo mostra, isso vale tanto se os dois rodam em sequência quanto ao mesmo tempo.

Guarde isto

Este é o único caso em que a cópia é incondicional. Em todo outro lugar, o Hive só copia quando precisa — que é o assunto da Parte V. Uma proc também pode pedir o mutex em vez de uma cópia dele, que é o próximo passo.

22A proc can ask for the mutex itselfUma proc pode pedir o mutex em si

The copy above is what an ordinary parameter gets. A proc — and only a proc — can ask for the mutex instead: write the parameter name: mut T and the callee is handed the caller's storage, not a view of it.

proc grow(log: mut Str[dyn], entry: Str): void {
	append(log, entry)
}

proc main(): void {
	mut Str[dyn] log = ["start"]
	grow(log, "opened")   // writes the caller's own vector
	echo log
}
prints
[start opened]

The argument has to be a mut variable, or a path into one. Both halves matter: there has to be storage to point at, and its owner has to have said it can change. A value (grow(["start"], "x")) and an immutable name are both refused.

func grow(log: mut Str[dyn]): void {   // a func cannot take one
	append(log, "x")
}
does not compile
func `grow` cannot receive a mutex: `log: mut ...` would let it write to
storage its caller can still see, and that is the side effect a func is
the absence of.
Keep in mind

Whether the callee gets that storage or a copy of it is decided by the call, not by the declaration — which is the same rule as everywhere else in Hive. Part XI has the other half.

A cópia acima é o que um parâmetro comum recebe. Uma proc — e só uma proc — pode pedir o mutex em vez disso: escreva o parâmetro nome: mut T e quem foi chamado recebe o armazenamento de quem chamou, não uma vista dele.

proc grow(log: mut Str[dyn], entry: Str): void {
	append(log, entry)
}

proc main(): void {
	mut Str[dyn] log = ["start"]
	grow(log, "opened")   // escreve no vetor de quem chamou
	echo log
}
imprime
[start opened]

O argumento tem que ser uma variável mut, ou um caminho até uma. As duas metades importam: tem que existir armazenamento para apontar, e o dono dele tem que ter dito que pode mudar. Um valor (grow(["start"], "x")) e um nome imutável são os dois recusados.

func grow(log: mut Str[dyn]): void {   // uma func não pode receber um
	append(log, "x")
}
não compila
func `grow` cannot receive a mutex: `log: mut ...` would let it write to
storage its caller can still see, and that is the side effect a func is
the absence of.
Guarde isto

Se quem foi chamado recebe esse armazenamento ou uma cópia dele é decidido pela chamada, não pela declaração — que é a mesma regra de todo o resto do Hive. A Parte XI tem a outra metade.

Part IVParte IV

VectorsVetores

Hive's one collection type — contiguous, typed, and bounds-checked before your program ever runs.O único tipo de coleção do Hive — contíguo, tipado, e com limites conferidos antes de o programa rodar.

23A vector literalUm literal de vetor

Square brackets, comma-separated. Every element has the same type, and that type is part of the vector's own type.

proc main(): void {
	names := ["Ada", "Grace", "Linus"]
	counts := [10, 9, 11]
	echo names
	echo counts
}
prints
[Ada Grace Linus]
[10 9 11]

A vector is memory-contiguous: the elements sit next to each other, and reading one is an offset rather than a chase through pointers.

Colchetes, separados por vírgula. Todo elemento tem o mesmo tipo, e esse tipo faz parte do tipo do próprio vetor.

proc main(): void {
	names := ["Ada", "Grace", "Linus"]
	counts := [10, 9, 11]
	echo names
	echo counts
}
imprime
[Ada Grace Linus]
[10 9 11]

Um vetor é contíguo na memória: os elementos ficam um ao lado do outro, e ler um é um deslocamento, não uma perseguição de ponteiros.

24Two kinds of length: Str[3] and Str[dyn]Dois tipos de tamanho: Str[3] e Str[dyn]

Written out, a vector type says how long it is. There are two answers it can give.

Str[2]   pair    = ["Hello", "World"]     // exactly two. Always.
Str[dyn] many    = ["Hello", "World"]     // any number, and it may grow
inferred        := ["Hello", "World"]     // two — read off the literal

A static length is a promise the compiler holds you to at every point a value could land in that slot. A dynamic length promises nothing, and is the one that can grow.

The difference is not documentation — it decides whether you may index without a guard. Steps 31 to 35 are entirely about that, because it is the part of Hive most likely to surprise you.

Note

A literal assigned to a [dyn] slot has to say so explicitly, which is why the second line above spells the type out. many := ["Hello", "World"] would give you a two-element vector, not a growable one.

Escrito por extenso, um tipo de vetor diz o tamanho dele. Existem duas respostas possíveis.

Str[2]   pair    = ["Hello", "World"]     // exatamente dois. Sempre.
Str[dyn] many    = ["Hello", "World"]     // qualquer número, e pode crescer
inferred        := ["Hello", "World"]     // dois — lido do literal

Um tamanho estático é uma promessa que o compilador cobra em todo ponto em que um valor possa cair naquele slot. Um tamanho dinâmico não promete nada, e é o que pode crescer.

A diferença não é documentação — ela decide se você pode indexar sem guarda. Os passos 31 a 35 são inteiramente sobre isso, porque é a parte do Hive com mais chance de te surpreender.

Nota

Um literal atribuído a um slot [dyn] precisa dizer isso explicitamente, e é por isso que a segunda linha acima escreve o tipo. many := ["Hello", "World"] daria um vetor de dois elementos, não um vetor que cresce.

25Str[]: the spelling for signaturesStr[]: a grafia para assinaturas

There is a third spelling, and it exists for signatures rather than storage. Str[] means "a vector of Str, some length" — it promises nothing and accepts anything of the right element type.

// Takes a Str[2], a Str[dyn], or anything else holding Strs.
func firstOf(values: Str[]): Str {
	if len(values) > 0 {
		return "first of {len(values)}: {values[0]}"
	}
	return "nothing at all"
}

One helper then serves a caller holding a Str[3] and a caller holding a Str[dyn], instead of being written twice. A parameter is the whole of where it is legal, and accepting either kind is the whole of what it is for.

A return may not be one. A return is where the caller is told what it is getting, and Str[] and Str[dyn] would be the same answer to them — no promise, every index guarded — while Str[3] is a very different one. Two spellings for one meaning only invite the reader to hunt for a difference that isn't there, so a return says [dyn] or it says a number.

Nor may a variable or a field. Those name real storage, and a promise is the only thing an index can rest on, so storage has to say which of the two real kinds it is.

Existe uma terceira grafia, e ela existe para assinaturas em vez de armazenamento. Str[] quer dizer "um vetor de Str, de algum tamanho" — não promete nada e aceita qualquer coisa do tipo de elemento certo.

// Recebe um Str[2], um Str[dyn], ou qualquer outra coisa que guarde Strs.
func firstOf(values: Str[]): Str {
	if len(values) > 0 {
		return "first of {len(values)}: {values[0]}"
	}
	return "nothing at all"
}

Um único helper então serve a quem tem um Str[3] e a quem tem um Str[dyn], em vez de ser escrito duas vezes. Um parâmetro é o lugar inteiro onde ele é legal, e aceitar os dois tipos é a razão inteira de ele existir.

Um retorno não pode ser um. Um retorno é onde quem chamou é informado do que vai receber, e Str[] e Str[dyn] seriam a mesma resposta para essa pessoa — promessa nenhuma, todo índice com guarda — enquanto Str[3] é uma resposta bem diferente. Duas grafias para um significado só convidam quem lê a caçar uma diferença que não existe, então um retorno diz [dyn] ou diz um número.

Nem pode ser uma variável ou um campo. Esses nomeiam armazenamento real, e uma promessa é a única coisa em que um índice pode se apoiar, então armazenamento tem que dizer qual dos dois tipos reais ele é.

26Measuring a vectorMedindo um vetor

The same two functions from step 9, asking the same two questions.

proc main(): void {
	counts := [10, 9, 11]
	echo len(counts)     // 3 — elements
	echo bytes(counts)   // 24 — the footprint of its storage
}

len counts elements. bytes reports the byte footprint of the contiguous storage — the element count times the element size.

As mesmas duas funções do passo 9, fazendo as mesmas duas perguntas.

proc main(): void {
	counts := [10, 9, 11]
	echo len(counts)     // 3 — elementos
	echo bytes(counts)   // 24 — a pegada do armazenamento dele
}

O len conta elementos. O bytes reporta a pegada em bytes do armazenamento contíguo — a contagem de elementos vezes o tamanho do elemento.

27Growing and shrinking: append, prepend, dropCrescendo e encolhendo: append, prepend, drop

append adds one element to the end, in place. It needs a target that is both mut and [dyn] — mutable because it changes the value, dynamic because a static length is a promise that cannot be grown out of.

proc main(): void {
	mut Str[dyn] shelf = ["Beeswax"]
	append(shelf, "Smoker fuel")
	append(shelf, "Hive tool")
	echo shelf
}
prints
[Beeswax Smoker fuel Hive tool]

It is a statement, not an expression — it returns nothing, and there is no new vector to catch.

Dynamic has to be declared

Notice the explicit Str[dyn] above. That is not decoration. A := binding reads its length off the value it was handed, and a length read off a value is a static one — so there is nothing there for append to grow.

does not compile
mut shelf := ["Beeswax"]
append(shelf, "Smoker fuel")

shelf holds exactly one element and the compiler knows it. Writing mut Str[dyn] shelf = ["Beeswax"] is how you ask for the other thing: a vector whose length is not part of what it promises, which is the only kind append can grow.

Two more builtins write through a vector the same way, and ask for exactly the same thing — a mut [dyn] vector. prepend is append's other end, and drop takes a range of elements out and hands them back.

proc main(): void {
	mut Str[dyn] queue = ["Smoker fuel", "Hive tool"]

	prepend(queue, "Beeswax")            // [Beeswax Smoker fuel Hive tool]

	if 1 < len(queue) {
		taken := drop(queue, 0, 1)   // takes the first two...
		echo taken                   // [Beeswax Smoker fuel]
	}
	echo queue                           // ...leaving [Hive tool]
}

prepend costs what its name implies: every element moves up one, so it is the length of the vector, where append is nothing. Like append it hands nothing back — the vector you gave it is the result — so it can only stand as a statement of its own.

drop is the one that answers with something: the elements it removed. Its two bounds are a range, inclusive at both ends like a slice's (step 30), and they are proven in range at compile time exactly as a slice's are (steps 31–32) — which is why the guard is there above. Crossed bounds take nothing and hand back an empty vector, the same as the slice they describe.

A drop costs a proof

It is the one builtin that makes a vector shorter. append only ever grows one, so a position already proven in range stays in range; after a drop it may simply not be there, so what was proven about that vector has to be proven again. The compiler tracks this for you — it is only worth knowing so the second guard does not look redundant.

O append adiciona um elemento ao fim, no lugar. Ele precisa de um alvo que seja ao mesmo tempo mut e [dyn] — mutável porque muda o valor, dinâmico porque um tamanho estático é uma promessa da qual não se pode crescer para fora.

proc main(): void {
	mut Str[dyn] shelf = ["Beeswax"]
	append(shelf, "Smoker fuel")
	append(shelf, "Hive tool")
	echo shelf
}
imprime
[Beeswax Smoker fuel Hive tool]

É um comando, não uma expressão — não retorna nada, e não há vetor novo para pegar.

Dinâmico tem que ser declarado

Repare no Str[dyn] explícito acima. Ele não é enfeite. Um vínculo com := lê o tamanho do valor que recebeu, e um tamanho lido de um valor é estático — então não há ali nada para o append fazer crescer.

não compila
mut shelf := ["Beeswax"]
append(shelf, "Smoker fuel")

shelf guarda exatamente um elemento e o compilador sabe disso. Escrever mut Str[dyn] shelf = ["Beeswax"] é como você pede a outra coisa: um vetor cujo tamanho não faz parte do que ele promete, que é o único tipo que o append consegue crescer.

Outros dois builtins escrevem através de um vetor do mesmo jeito, e pedem exatamente a mesma coisa — um vetor mut [dyn]. O prepend é a outra ponta do append, e o drop tira uma faixa de elementos de dentro e a devolve.

proc main(): void {
	mut Str[dyn] queue = ["Smoker fuel", "Hive tool"]

	prepend(queue, "Beeswax")            // [Beeswax Smoker fuel Hive tool]

	if 1 < len(queue) {
		taken := drop(queue, 0, 1)   // tira os dois primeiros...
		echo taken                   // [Beeswax Smoker fuel]
	}
	echo queue                           // ...deixando [Hive tool]
}

O prepend custa o que o nome dele sugere: todos os elementos andam uma posição, então ele custa o tamanho do vetor, enquanto o append não custa nada. Como o append, ele não devolve nada — o vetor que você deu é o resultado — então só pode ficar num comando só dele.

O drop é o que responde com algo: os elementos que removeu. Os dois limites dele são uma faixa, inclusiva nas duas pontas como num slice (passo 30), e são provados dentro do alcance em tempo de compilação exatamente como os de um slice (passos 31–32) — que é por isso que a guarda está ali acima. Limites cruzados não tiram nada e devolvem um vetor vazio, igualzinho ao slice que descrevem.

Um drop custa uma prova

Ele é o único builtin que deixa um vetor menor. O append apenas cresce, então uma posição já provada continua provada; depois de um drop ela pode simplesmente não estar mais lá, e o que era provado sobre aquele vetor tem de ser provado de novo. O compilador cuida disso para você — só vale saber para a segunda guarda não parecer redundante.

28Concatenating with +Concatenando com +

+ between two vectors builds a brand-new vector. Neither side is touched, and neither needs to be mutable.

proc main(): void {
	greeting := ["Hello", "darkness"] + ["my", "old", "friend"]
	echo len(greeting)
}
prints
5

So there are two ways to make a vector bigger, and they are genuinely different: append and prepend grow the one you have, + gives you a new one.

O + entre dois vetores constrói um vetor novinho. Nenhum dos lados é mexido, e nenhum precisa ser mutável.

proc main(): void {
	greeting := ["Hello", "darkness"] + ["my", "old", "friend"]
	echo len(greeting)
}
imprime
5

Então há dois jeitos de aumentar um vetor, e eles são realmente diferentes: o append e o prepend crescem o que você tem, o + te dá um novo.

29Comparing vectorsComparando vetores

== and != compare vectors structurally: same length first, then element by element, stopping at the first difference.

proc main(): void {
	echo ["a", "b"] == ["a", "b"]     // true
	echo ["a", "b"] == ["a"]          // false — lengths differ
	echo [[1, 2], [3]] == [[1, 2], [3]]   // true — nesting compares too
}

Nested vectors and tables compare the same way, all the way down. Comparing a vector against something that is not a vector is a compile error rather than a quiet false.

== e != comparam vetores estruturalmente: primeiro o mesmo tamanho, depois elemento por elemento, parando na primeira diferença.

proc main(): void {
	echo ["a", "b"] == ["a", "b"]     // true
	echo ["a", "b"] == ["a"]          // false — tamanhos diferentes
	echo [[1, 2], [3]] == [[1, 2], [3]]   // true — aninhamento também compara
}

Vetores aninhados e tabelas comparam do mesmo jeito, até o fundo. Comparar um vetor com algo que não é vetor é erro de compilação, e não um false silencioso.

30Slicing — and its inclusive high boundSlice — e seu limite superior inclusivo

A slice takes a run of elements. v[low:high], and either end may be left off.

rows := table[1:]        // everything from index 1 to the last row
Read this twice

Hive's high bound is inclusive. v[1:3] is the elements at indexes 1, 2 and 3 — three of them. If you have written Python, Go or Rust, this is the one place your instincts will be off by one.

So v[0:1] is a two-element slice, and the open form v[1:] is shorthand for "index 1 through the last one" — which is the usual way to drop a header row from a table.

Like every other index, a slice has to be provably in range at compile time. That is the next step.

Um slice pega um trecho de elementos. v[baixo:alto], e qualquer uma das pontas pode ser omitida.

rows := table[1:]        // tudo do índice 1 até a última linha
Leia duas vezes

O limite superior do Hive é inclusivo. v[1:3] são os elementos nos índices 1, 2 e 3 — três deles. Se você já escreveu Python, Go ou Rust, este é o único lugar em que seus instintos vão errar por um.

Então v[0:1] é um slice de dois elementos, e a forma aberta v[1:] é atalho para "do índice 1 até o último" — que é o jeito usual de descartar a linha de cabeçalho de uma tabela.

Como todo índice, um slice tem que ser provadamente válido em tempo de compilação. Esse é o próximo passo.

31Indexing is checked before the program runsÍndices são conferidos antes de o programa rodar

Hive has a dedicated pass whose only job is proving that every index and every slice is in range. If it cannot prove one, the program does not compile — so a Hive program never fails at runtime with an out-of-range index.

On a static length, that proof is arithmetic:

Str[3] parts = ["a", "b", "c"]
echo parts[2]      // fine: 2 is less than 3
echo parts[3]      // compile error: there is no index 3

On a dynamic length there is nothing to do arithmetic with, so you have to show the compiler a guard it can read:

proc main(): void {
	Str[dyn] shelf = ["Beeswax", "Smoker fuel"]
	if 0 < len(shelf) {
		echo shelf[0]        // proven inside the branch
	}
}
does not compile
Str[dyn] shelf = ["Beeswax"]
echo shelf[0]

Nothing in sight says shelf has an element 0. It plainly does, one line above — but the rule is a guard the pass can see, and holding to it without exception is what makes the guarantee worth having.

Three shapes count as proof: a literal index into a static length, a guard like the one above, and the condition of a counting for loop. A for each loop needs nothing, because it never indexes at all.

O Hive tem uma passagem dedicada cujo único trabalho é provar que todo índice e todo slice está na faixa. Se ela não conseguir provar um, o programa não compila — então um programa Hive nunca falha em execução por índice fora da faixa.

Num tamanho estático, essa prova é aritmética:

Str[3] parts = ["a", "b", "c"]
echo parts[2]      // ok: 2 é menor que 3
echo parts[3]      // erro de compilação: não existe índice 3

Num tamanho dinâmico não há nada com que fazer aritmética, então você precisa mostrar ao compilador uma guarda que ele consiga ler:

proc main(): void {
	Str[dyn] shelf = ["Beeswax", "Smoker fuel"]
	if 0 < len(shelf) {
		echo shelf[0]        // provado dentro do bloco
	}
}
não compila
Str[dyn] shelf = ["Beeswax"]
echo shelf[0]

Nada à vista diz que shelf tem um elemento 0. Ele claramente tem, uma linha acima — mas a regra é uma guarda que a passagem consiga ver, e sustentar isso sem exceção é o que faz a garantia valer a pena.

Três formas contam como prova: um índice literal num tamanho estático, uma guarda como a de cima, e a condição de um for de contagem. Um for each não precisa de nada, porque ele nunca indexa.

32The bounds shorthandO atalho bounds

Writing i >= 0 && i < len(v) gets old quickly, so there is a keyword for exactly that condition.

if bank bounds i {
	echo bank[i]
}

// means precisely:
if i >= 0 && i < len(bank) {
	echo bank[i]
}

It is a plain boolean expression, so it combines like any other condition: if bank bounds i && bank bounds j && bank[i] > bank[j] { … }.

Note that a variable index needs the >= 0 half too — which is easy to forget by hand, and never forgotten by bounds.

Escrever i >= 0 && i < len(v) cansa rápido, então existe uma palavra-chave para exatamente essa condição.

if bank bounds i {
	echo bank[i]
}

// quer dizer precisamente:
if i >= 0 && i < len(bank) {
	echo bank[i]
}

É uma expressão booleana comum, então combina como qualquer outra condição: if bank bounds i && bank bounds j && bank[i] > bank[j] { … }.

Repare que um índice variável precisa também da metade >= 0 — que é fácil de esquecer à mão, e nunca é esquecida pelo bounds.

33A declared length is a promise, not a hintUm tamanho declarado é promessa, não dica

Str[3] means three. Everywhere. Every value that lands in such a slot — an initialiser, a later assignment, an argument, a field of a constructed value, a returned value — has to be a vector of exactly that many elements.

mut Str[3] v = ["a", "b", "c"]
v = ["x", "y", "z"]              // fine — still three
v = ["x"]                        // compile error: v is declared Str[3]
Str[3] parts = split(line, ",")  // compile error: that length isn't known

The last line is the interesting one. split returns a vector of some length; nothing says three. A length the compiler cannot see is rejected as firmly as a length it can see is wrong.

The payoff is that the promise is never lost. v[2] above stays legal after the reassignment, and a Str[3] parameter can be indexed inside the callee with no guard, because every call site was already held to it.

Str[3] quer dizer três. Em todo lugar. Todo valor que cai nesse slot — um inicializador, uma atribuição depois, um argumento, um campo de um valor construído, um valor retornado — tem que ser um vetor de exatamente essa quantidade.

mut Str[3] v = ["a", "b", "c"]
v = ["x", "y", "z"]              // ok — ainda três
v = ["x"]                        // erro: v é declarado Str[3]
Str[3] parts = split(line, ",")  // erro: esse tamanho não é conhecido

A última linha é a interessante. O split retorna um vetor de algum tamanho; nada diz três. Um tamanho que o compilador não consegue ver é rejeitado com a mesma firmeza com que um tamanho que ele consegue ver está errado.

O ganho é que a promessa nunca se perde. O v[2] acima continua legal depois da reatribuição, e um parâmetro Str[3] pode ser indexado dentro de quem foi chamado sem guarda, porque todo ponto de chamada já foi cobrado.

34An inferred length is weakerUm tamanho inferido é mais fraco

When you let the compiler read the length off a literal, it knows the length today — but nothing constrains what comes next, so a rebinding costs it.

mut v := ["a", "b", "c"]
echo v[2]                   // fine — three, as inferred
v[0] = "x"                  // still three: a write swaps, it doesn't resize
does not compile
mut v := ["a", "b", "c"]
if changed { v = ["x"] }
echo v[2]

The branch may not even run — but it may, and after it the length is no longer known. A proof that holds only sometimes is not a proof.

Declare the type (mut Str[3] v = …) when you want the promise enforced, and [dyn] when you want the freedom and will guard your indexes.

And an inferred length is a static length, so it is not one append can grow — growing needs the explicit [dyn] from step 27.

Quando você deixa o compilador ler o tamanho de um literal, ele sabe o tamanho de hoje — mas nada restringe o que vem depois, então uma reatribuição custa isso.

mut v := ["a", "b", "c"]
echo v[2]                   // ok — três, como inferido
v[0] = "x"                  // ainda três: uma escrita troca, não redimensiona
não compila
mut v := ["a", "b", "c"]
if changed { v = ["x"] }
echo v[2]

O bloco pode nem rodar — mas pode, e depois dele o tamanho não é mais conhecido. Uma prova que vale só às vezes não é prova.

Declare o tipo (mut Str[3] v = …) quando você quer a promessa cobrada, e [dyn] quando quer a liberdade e vai guardar seus índices.

E um tamanho inferido é um tamanho estático, então não é um que o append consiga crescer — crescer exige o [dyn] explícito do passo 27.

35A static parameter restricts the callable as a valueUm parâmetro estático restringe o callable como valor

A callable can be used as a value — that is Part VII. But keeping a promise means keeping it at every call site, so a callable with a statically-sized parameter is limited in how far it may travel.

proc takes(v: Str[3]): void { echo v[2] }

f := takes
f(["a", "b", "c"])               // fine — checked exactly like a direct call
f(["a"])                         // compile error: f holds a Str[3] taker

You may bind it to an immutable name and call through it. You may not pass it on, return it, or store it in a vector or a field, because the eventual call would happen somewhere with no idea what was promised. A mut holder is out for the same reason: it could be pointed at a different callable afterwards.

Declaring the parameter Str[dyn] or Str[] lifts every one of these restrictions, at the cost of a guard inside the callee. That is the trade, stated once.

Um callable pode ser usado como valor — isso é a Parte VII. Mas manter uma promessa significa mantê-la em todo ponto de chamada, então um callable com parâmetro de tamanho estático é limitado em quão longe pode viajar.

proc takes(v: Str[3]): void { echo v[2] }

f := takes
f(["a", "b", "c"])               // ok — conferido exatamente como uma chamada direta
f(["a"])                         // erro: f guarda um receptor de Str[3]

Você pode vinculá-lo a um nome imutável e chamar por ele. Você não pode repassá-lo, retorná-lo, nem guardá-lo num vetor ou num campo, porque a chamada eventual aconteceria em um lugar sem ideia do que foi prometido. Um detentor mut está fora pelo mesmo motivo: ele poderia ser apontado para outro callable depois.

Declarar o parâmetro Str[dyn] ou Str[] levanta cada uma dessas restrições, ao custo de uma guarda dentro de quem foi chamado. Essa é a troca, dita uma vez.

36join and splitjoin e split

The two halves of turning text into a vector and back.

proc main(): void {
	parts := split("Beeswax,Smoker fuel,Hive tool", ",")
	echo len(parts)
	echo join(parts, " | ")
}
prints
3
Beeswax | Smoker fuel | Hive tool

split hands back a Str vector of unknown length — which is why step 33's Str[3] parts = split(…) was refused. Splitting on "" gives you one element per character, which is the usual way to walk a string.

As duas metades de transformar texto em vetor e de volta.

proc main(): void {
	parts := split("Beeswax,Smoker fuel,Hive tool", ",")
	echo len(parts)
	echo join(parts, " | ")
}
imprime
3
Beeswax | Smoker fuel | Hive tool

O split devolve um vetor de Str de tamanho desconhecido — que é por que o Str[3] parts = split(…) do passo 33 foi recusado. Dividir por "" dá um elemento por caractere, que é o jeito usual de caminhar por uma string.

37indexOf answers with a ResultindexOf responde com um Result

Searching can fail, so indexOf does not return a number. It returns a Result: Ok(i) carrying the position, or an Error.

proc main(): void {
	names := ["Ada", "Grace", "Linus"]
	found := indexOf(names, "Grace")
	if found is Result.Ok(i) {
		echo "Grace is at {i}"
	} else if found is Result.Error(_) {
		echo "no Grace here"
	}
}
prints
Grace is at 1

(Step 71 covers is properly; for now read it as "if this Result is an Ok, call its contents i".)

The error payload is just false — there is nothing to say about a miss beyond that it missed. On a Str it searches for a substring and counts positions in characters, lining up with what len reports there.

Buscar pode falhar, então o indexOf não retorna um número. Ele retorna um Result: Ok(i) carregando a posição, ou um Error.

proc main(): void {
	names := ["Ada", "Grace", "Linus"]
	found := indexOf(names, "Grace")
	if found is Result.Ok(i) {
		echo "Grace is at {i}"
	} else if found is Result.Error(_) {
		echo "no Grace here"
	}
}
imprime
Grace is at 1

(O passo 71 cobre o is de verdade; por ora leia como "se este Result é um Ok, chame o conteúdo dele de i".)

O payload do erro é apenas false — não há nada a dizer sobre uma busca perdida além de que ela se perdeu. Num Str ele busca uma substring e conta posições em caracteres, alinhado com o que o len reporta ali.

38…and that index needs no guard…e esse índice não precisa de guarda

Here is why indexOf returns a position rather than -1: an Ok payload is always a position the vector really has. The bounds pass knows that, so the index arrives already proven.

found := indexOf(names, "bob")
if found is Result.Ok(i) {
	echo "{i}: {names[i]}"     // no guard of your own needed
} else if found is Result.Error(_) {
	echo "no bob here"
}

The proof is tied to that vector, and only while it means the same thing: an index found in a still needs a guard to index b, and rebinding the vector between the search and the use drops it.

Searching an empty Str never succeeds — not even for an empty needle — because that would hand back an index pointing at nothing.

Aqui está por que o indexOf retorna uma posição em vez de -1: o payload de um Ok é sempre uma posição que o vetor realmente tem. A passagem de limites sabe disso, então o índice chega já provado.

found := indexOf(names, "bob")
if found is Result.Ok(i) {
	echo "{i}: {names[i]}"     // nenhuma guarda sua é necessária
} else if found is Result.Error(_) {
	echo "no bob here"
}

A prova é amarrada àquele vetor, e só enquanto ele significar a mesma coisa: um índice achado em a ainda precisa de guarda para indexar b, e reatribuir o vetor entre a busca e o uso derruba a prova.

Buscar num Str vazio nunca dá certo — nem para uma agulha vazia — porque isso entregaria um índice apontando para nada.

39Table is a vector of vectorsTable é um vetor de vetores

Tables are central enough to Hive to have a name, but not a separate type. Table is an alias for Str[dyn][dyn] — a dynamic vector of dynamic vectors of Str. Rows of cells.

Table headers = [
	["Content-Type", "application/json"],
	["Accept", "text/plain"],
]

Because it is an alias and not a special case, everything you have learned already applies: len gives the row count, t[0] is the first row (guarded), t[1:] drops the header, == compares it structurally, and append adds a row.

Every table Hive hands you — a CSV, a spreadsheet sheet, an untyped SQL result, HTTP headers — is this one type. That is what makes them interchangeable.

Tabelas são centrais o suficiente no Hive para ter um nome, mas não um tipo separado. Table é um apelido para Str[dyn][dyn] — um vetor dinâmico de vetores dinâmicos de Str. Linhas de células.

Table headers = [
	["Content-Type", "application/json"],
	["Accept", "text/plain"],
]

Como é um apelido e não um caso especial, tudo que você já aprendeu se aplica: len dá a contagem de linhas, t[0] é a primeira linha (com guarda), t[1:] descarta o cabeçalho, == compara estruturalmente, e append adiciona uma linha.

Toda tabela que o Hive te entrega — um CSV, uma aba de planilha, um resultado SQL sem tipo, cabeçalhos HTTP — é este mesmo tipo. É isso que os torna intercambiáveis.

40Looking things up: row and columnBuscando: row e column

Two builtins for the common case of a table with headings.

// stock holds:  item | restocked  | pence
//               Beeswax | 2026-07-02 | 450
echo join(row(stock, "Beeswax"), " | ")
echo join(column(stock, "item"), ", ")
prints
Beeswax | 2026-07-02 | 450
item, Beeswax, Smoker fuel, Hive tool

row finds the row whose first cell equals the key. column finds the column whose top cell does — so the heading cell it matched on comes back at the front of the result, as above.

Neither fails. A key that matches nothing yields an empty vector, and column skips any row too short to reach the matched column.

Dois builtins para o caso comum de uma tabela com cabeçalhos.

// stock contém:  item | restocked  | pence
//                Beeswax | 2026-07-02 | 450
echo join(row(stock, "Beeswax"), " | ")
echo join(column(stock, "item"), ", ")
imprime
Beeswax | 2026-07-02 | 450
item, Beeswax, Smoker fuel, Hive tool

O row acha a linha cuja primeira célula é igual à chave. O column acha a coluna cuja célula do topo é — então a célula de cabeçalho que ele casou volta na frente do resultado, como acima.

Nenhum dos dois falha. Uma chave que não casa com nada entrega um vetor vazio, e o column pula qualquer linha curta demais para alcançar a coluna casada.

Part VParte V

Values that behave like valuesValores que se comportam como valores

The rule that decides when binding a vector to a second name copies it — and when it deliberately does not.A regra que decide quando vincular um vetor a um segundo nome o copia — e quando deliberadamente não copia.

41The problem this part solvesO problema que esta parte resolve

Vectors, tables and structs that contain them are value types in Hive. Binding one to a second name is conceptually a copy, so a later change to one side is never observed through the other.

Underneath, though, they are contiguous blocks of memory reached through a header — and two headers can point at the same block. If Hive did nothing, the second name would be a window onto the first, and a value you thought was standing still would change under you.

mut original := [1, 2, 3]
snapshot := original       // is this a second value, or a second window?
original[0] = 99
echo snapshot              // [1 2 3], and Hive guarantees it

The obvious fix — copy on every binding — is correct and wasteful. So Hive copies only when a copy is the only way to keep that guarantee, and the rest of the time it shares the storage. Which one you get is decided at compile time, per binding, and the next step is the whole rule.

Vetores, tabelas e structs que os contêm são tipos de valor no Hive. Vincular um a um segundo nome é, conceitualmente, uma cópia, então uma mudança depois em um lado nunca é observada pelo outro.

Por baixo, porém, eles são blocos contíguos de memória alcançados por um header — e dois headers podem apontar para o mesmo bloco. Se o Hive não fizesse nada, o segundo nome seria uma janela para o primeiro, e um valor que você pensava estar parado mudaria embaixo de você.

mut original := [1, 2, 3]
snapshot := original       // isto é um segundo valor, ou uma segunda janela?
original[0] = 99
echo snapshot              // [1 2 3], e o Hive garante isso

A correção óbvia — copiar em toda vinculação — é correta e desperdiçada. Então o Hive só copia quando copiar é o único jeito de manter essa garantia, e no resto do tempo compartilha o armazenamento. Qual dos dois você recebe é decidido em tempo de compilação, por vinculação, e o próximo passo é a regra inteira.

42The rule: share or copyA regra: compartilhar ou copiar

Only in-place writes can break value semantics, and only a mut name can write in place. So the invariant to keep is a small one: storage that an immutable name observes is never mutated in place afterwards. Each binding is classified by the mutability of its two ends.

target ⟵ sourcedecision
immutable ⟵ immutableshare — neither side can ever mutate it
mutmutshare — shared mutable state is the intent
mut ⟵ immutableshare if the target is never written through, else copy
immutable ⟵ mutshare if the source is never mutated again, else copy

Sharing is only ever chosen when it is provably indistinguishable from copying. The two interesting rows are the mixed ones, and the next three steps run each of them as a program you can execute.

Só escritas no lugar podem quebrar a semântica de valor, e só um nome mut pode escrever no lugar. Então a invariante a manter é pequena: armazenamento que um nome imutável observa nunca é mutado no lugar depois disso. Cada vinculação é classificada pela mutabilidade das suas duas pontas.

destino ⟵ origemdecisão
imutável ⟵ imutávelcompartilha — nenhum dos lados pode mutar
mutmutcompartilha — estado mutável compartilhado é a intenção
mut ⟵ imutávelcompartilha se o destino nunca for escrito, senão copia
imutável ⟵ mutcompartilha se a origem não for mais mutada, senão copia

Compartilhar só é escolhido quando é provadamente indistinguível de copiar. As duas linhas interessantes são as mistas, e os próximos três passos rodam cada uma delas como um programa que você pode executar.

A fresh right-hand side is never copied, because there is nothing to share with: a literal, a + concatenation or a function's return value is already nobody else's.

Um lado direito novo nunca é copiado, porque não há nada com que compartilhar: um literal, uma concatenação com + ou o retorno de uma função já não é de mais ninguém.

43An immutable name is a snapshotUm nome imutável é um snapshot

Row four of the table, in a program you can run.

proc main(): void {
	mut original := [1, 2, 3]
	snapshot := original      // copied: `original` is mutated below

	original[0] = 99

	echo snapshot
	echo original
}
prints
[1 2 3]
[99 2 3]

This is the behaviour you would want without being told, which is rather the point. What is worth knowing is that the copy was inserted because of that write — delete the original[0] = 99 line and the two names quietly share one block, with identical observable behaviour and no copy at all.

A quarta linha da tabela, num programa que você pode rodar.

proc main(): void {
	mut original := [1, 2, 3]
	snapshot := original      // copiado: `original` é mutado abaixo

	original[0] = 99

	echo snapshot
	echo original
}
imprime
[1 2 3]
[99 2 3]

Este é o comportamento que você iria querer sem que ninguém dissesse, que é justamente o ponto. O que vale saber é que a cópia foi inserida por causa daquela escrita — apague a linha original[0] = 99 e os dois nomes compartilham um bloco silenciosamente, com comportamento observável idêntico e nenhuma cópia.

44Two mut names share, completelyDois nomes mut compartilham, por completo

Row two. Two mutable names always share, because that is the way to ask for shared mutable state.

proc main(): void {
	mut a := [1, 2, 3]
	mut b := a                // shared: both sides are mut

	b[0] = 99

	echo a
	echo b
}
prints
[99 2 3]
[99 2 3]

And the sharing is complete, which is stronger than it may look. Every change through either name is visible through the other, including the ones that change the length:

mut Str[dyn] a = ["x", "y", "z"]
mut Str[dyn] b = a
append(b, "w")            // len(a) is now 4
if 0 < len(a) {
	a[0] = "changed"      // b[0] is "changed" too
}
b = ["replaced"]          // rebinding one rebinds both; len(a) is 1
How that last line works

Two independent headers could not deliver this — growing one would quietly stop the two from sharing. So the second name is not given storage of its own at all: b compiles to a. There is one header, under two names, which is why even a rebinding is seen through both.

The exception is a source that does not name the same storage each time it is read — mut b = a[i] can be a different element every time i moves — so that binding keeps a header of its own.

A segunda linha. Dois nomes mutáveis sempre compartilham, porque esse é o jeito de pedir estado mutável compartilhado.

proc main(): void {
	mut a := [1, 2, 3]
	mut b := a                // compartilhado: os dois lados são mut

	b[0] = 99

	echo a
	echo b
}
imprime
[99 2 3]
[99 2 3]

E o compartilhamento é completo, o que é mais forte do que parece. Toda mudança por qualquer um dos nomes é visível pelo outro, inclusive as que mudam o tamanho:

mut Str[dyn] a = ["x", "y", "z"]
mut Str[dyn] b = a
append(b, "w")            // len(a) agora é 4
if 0 < len(a) {
	a[0] = "changed"      // b[0] também é "changed"
}
b = ["replaced"]          // reatribuir um reatribui os dois; len(a) é 1
Como aquela última linha funciona

Dois headers independentes não entregariam isso — crescer um faria os dois pararem de compartilhar silenciosamente. Então o segundo nome não recebe armazenamento próprio nenhum: b compila para a. Existe um header, sob dois nomes, que é por que até uma reatribuição é vista pelos dois.

A exceção é uma origem que não nomeia o mesmo armazenamento a cada leitura — mut b = a[i] pode ser um elemento diferente cada vez que i muda — então essa vinculação mantém um header próprio.

45When a copy happens, it is deepQuando uma cópia acontece, ela é profunda

A copy is not one level. Nested vectors, tables and the vector fields of a struct are all copied, so the snapshot is independent all the way down.

proc main(): void {
	mut grid := [[1, 2], [3, 4]]
	snapshot := grid          // deep-copied: every row gets its own storage

	mut firstRow := grid[0]
	if 0 < len(firstRow) {
		firstRow[0] = 99
	}

	echo snapshot
	echo grid
}
prints
[[1 2] [3 4]]
[[99 2] [3 4]]

The depth is decided by the type, at compile time — the compiler knows a Int[dyn][dyn] has two levels and a Basket has one vector field, and emits exactly that much copying. Nothing inspects the value at runtime, and a type made only of scalars needs no copying code at all.

Uma cópia não é de um nível só. Vetores aninhados, tabelas e os campos vetoriais de um struct são todos copiados, então o snapshot é independente até o fundo.

proc main(): void {
	mut grid := [[1, 2], [3, 4]]
	snapshot := grid          // copiado em profundidade: cada linha ganha armazenamento próprio

	mut firstRow := grid[0]
	if 0 < len(firstRow) {
		firstRow[0] = 99
	}

	echo snapshot
	echo grid
}
imprime
[[1 2] [3 4]]
[[99 2] [3 4]]

A profundidade é decidida pelo tipo, em tempo de compilação — o compilador sabe que um Int[dyn][dyn] tem dois níveis e que um Basket tem um campo vetorial, e emite exatamente essa quantidade de cópia. Nada inspeciona o valor em tempo de execução, e um tipo feito só de escalares não precisa de código de cópia nenhum.

46When in doubt, it copiesNa dúvida, copia

The analysis has to be conservative, because being wrong in one direction is much worse than the other. If a name escapes — into a function call, or into a constructed value — the compiler treats it as possibly-mutated and copies.

mut rows := [["a"], ["b"]]
snapshot := rows          // copies: `rows` escapes into the call below
consume(rows)

Being wrong this way costs a copy. Being wrong the other way would silently break value semantics, which is not a trade worth making. You will meet the same conservatism again in step 136, where a different analysis makes the same call for the same reason.

Runnable walkthrough of all four rows: code-examples/6 - Value Semantics/

A análise tem que ser conservadora, porque errar numa direção é muito pior que na outra. Se um nome escapa — para dentro de uma chamada de função, ou de um valor construído — o compilador o trata como possivelmente mutado e copia.

mut rows := [["a"], ["b"]]
snapshot := rows          // copia: `rows` escapa na chamada abaixo
consume(rows)

Errar desse lado custa uma cópia. Errar do outro quebraria silenciosamente a semântica de valor, que não é uma troca que valha a pena. Você vai encontrar o mesmo conservadorismo de novo no passo 136, onde uma análise diferente decide do mesmo jeito e pelo mesmo motivo.

Passo a passo executável das quatro linhas: code-examples/6 - Value Semantics/
Part VIParte VI

Control flowControle de fluxo

Branching, two kinds of loop, and the two ways to stop a program on purpose.Ramificação, dois tipos de laço, e os dois jeitos de parar um programa de propósito.

47if, else if, elseif, else if, else

No parentheses around the condition, and the braces are not optional.

proc main(): void {
	frames := 9

	if frames > 10 {
		echo "a strong colony"
	} else if frames > 4 {
		echo "coming along"
	} else {
		echo "needs feeding"
	}
}
prints
coming along

The condition is any expression that produces a Bool — a comparison, a Bool variable, an is test (Part IX), or several of those combined.

Sem parênteses em volta da condição, e as chaves não são opcionais.

proc main(): void {
	frames := 9

	if frames > 10 {
		echo "a strong colony"
	} else if frames > 4 {
		echo "coming along"
	} else {
		echo "needs feeding"
	}
}
imprime
coming along

A condição é qualquer expressão que produza um Bool — uma comparação, uma variável Bool, um teste is (Parte IX), ou várias dessas combinadas.

48Combining conditionsCombinando condições

&& is and, || is or, and both short-circuit — the right side is not evaluated when the left already settles it.

if frames > 4 && frames < 11 {
	echo "in the usual range"
}

if frames == 0 || abandoned {
	echo "nothing to inspect"
}

Short-circuiting is not just an optimisation here. In Part IX you will bind a value in the left half of an && and use it in the right half, which only makes sense because the right half runs strictly after — and only if — the left one succeeded.

&& é "e", || é "ou", e os dois fazem curto-circuito — o lado direito não é avaliado quando o esquerdo já resolve.

if frames > 4 && frames < 11 {
	echo "in the usual range"
}

if frames == 0 || abandoned {
	echo "nothing to inspect"
}

O curto-circuito aqui não é só otimização. Na Parte IX você vai vincular um valor na metade esquerda de um && e usá-lo na metade direita, o que só faz sentido porque a direita roda estritamente depois — e só se — a esquerda deu certo.

49The counting loopO laço de contagem

Three clauses separated by semicolons: run this once, keep going while this holds, do this after every pass.

proc main(): void {
	for i := 0; i < 3; i++ {
		echo "pass {i}"
	}
}
prints
pass 0
pass 1
pass 2

The counter is scoped to the loop and implicitly mutable, so it needs no muti++ is allowed even though i was declared with :=. It also does not exist after the loop ends.

One more thing this loop does for free: its condition is proof for the bounds pass. Inside for i := 0; i < len(v); i++, the expression v[i] needs no guard.

Três cláusulas separadas por ponto e vírgula: rode isto uma vez, continue enquanto isto valer, faça isto depois de cada passada.

proc main(): void {
	for i := 0; i < 3; i++ {
		echo "pass {i}"
	}
}
imprime
pass 0
pass 1
pass 2

O contador tem escopo no laço e é implicitamente mutável, então não precisa de mut — o i++ é permitido mesmo que i tenha sido declarado com :=. Ele também não existe depois de o laço terminar.

Mais uma coisa que este laço faz de graça: a condição dele é prova para a passagem de limites. Dentro de for i := 0; i < len(v); i++, a expressão v[i] não precisa de guarda.

50for each: walking a vectorfor each: caminhando um vetor

When you want the elements and not the positions — which is most of the time — this is the loop to reach for.

proc main(): void {
	for each item in ["Beeswax", "Smoker fuel", "Hive tool"] {
		echo item
	}
}
prints
Beeswax
Smoker fuel
Hive tool

The name is bound to each element in turn, immutably, and its type is inferred from the vector. You can annotate it if you would rather be explicit: for each item: Str in shelf { … }.

Because it never indexes, it never needs a guard — which makes it the simplest way to touch every element of a [dyn] vector.

Quando você quer os elementos e não as posições — que é a maior parte das vezes — este é o laço a usar.

proc main(): void {
	for each item in ["Beeswax", "Smoker fuel", "Hive tool"] {
		echo item
	}
}
imprime
Beeswax
Smoker fuel
Hive tool

O nome é vinculado a cada elemento por vez, imutavelmente, e o tipo dele é inferido do vetor. Você pode anotar, se preferir ser explícito: for each item: Str in shelf { … }.

Como ele nunca indexa, nunca precisa de guarda — o que faz dele o jeito mais simples de tocar todo elemento de um vetor [dyn].

51Leaving loop clauses outOmitindo cláusulas do laço

Any of the counting loop's three clauses may be omitted, and the semicolons are what mark their places.

// A while loop: no initialiser, no step.
for ; len(out) < width; {
	out = out + " "
}

// Forever, until something inside stops it.
for ;; {
	line := hive.net.socketReceiveLine(connection)
	if line is Result.Error(error) {
		return
	}
}

for ;; is the idiom for a server loop, and you will see it again in Part XIV. It is not an infinite loop with an escape bolted on — the return or break inside is the exit condition, stated where it actually happens.

Qualquer uma das três cláusulas do laço de contagem pode ser omitida, e os pontos e vírgulas são o que marca os lugares delas.

// Um laço while: sem inicializador, sem passo.
for ; len(out) < width; {
	out = out + " "
}

// Para sempre, até que algo dentro pare.
for ;; {
	line := hive.net.socketReceiveLine(connection)
	if line is Result.Error(error) {
		return
	}
}

for ;; é o idioma para um laço de servidor, e você o vê de novo na Parte XIV. Não é um laço infinito com uma saída aparafusada — o return ou break lá dentro é a condição de saída, escrita onde ela realmente acontece.

52break and continuebreak e continue

continue skips to the next iteration; break leaves the loop entirely. Both act on the innermost enclosing loop.

proc main(): void {
	for each n in [1, 2, 3, 4, 5, 6] {
		if n % 2 != 0 {
			continue          // odd: skip it
		}
		if n > 4 {
			break             // done looking
		}
		echo n
	}
}
prints
2
4

Using either outside a loop is a compile error, not a no-op.

continue pula para a próxima iteração; break sai do laço por completo. Os dois agem no laço mais interno que os envolve.

proc main(): void {
	for each n in [1, 2, 3, 4, 5, 6] {
		if n % 2 != 0 {
			continue          // ímpar: pule
		}
		if n > 4 {
			break             // já vimos o suficiente
		}
		echo n
	}
}
imprime
2
4

Usar qualquer um dos dois fora de um laço é erro de compilação, não um no-op.

53assertassert

assert takes a condition and stops the program if it is false. It is the way to state something you believe must hold.

proc main(): void {
	total := partOne()
	assert total == 1227775554
	echo "Success"
}

The shipped Advent of Code examples use it exactly like this — as a test that lives in the program. If the answer ever stops matching, the program says so instead of printing something plausible.

O assert recebe uma condição e para o programa se ela for falsa. É o jeito de afirmar algo que você acredita que precisa valer.

proc main(): void {
	total := partOne()
	assert total == 1227775554
	echo "Success"
}

Os exemplos de Advent of Code que acompanham o projeto usam exatamente assim — como um teste que mora dentro do programa. Se a resposta parar de casar, o programa avisa em vez de imprimir algo plausível.

54panicpanic

panic stops the program immediately, showing the value you give it. Unlike assert it always fires, and it takes any value rather than a condition.

if hive.syslink.listen(me) is Result.Error(err) {
	panic err
}

The value is rendered exactly the way echo renders it, so panic err prints the error's message and panic #Missing prints Missing rather than a number. That makes it a reasonable thing to reach for when a failure genuinely means the program cannot continue.

Because it never returns, a branch ending in panic counts as a terminating path — which is the subject of the next step, and makes panic "unreachable" a legitimate way to close off a tail you know can never be reached.

One exception

Inside a hive.syslink service, a panic kills only that service and leaves the rest of the program running. Step 133 explains why that is the whole point.

O panic para o programa imediatamente, mostrando o valor que você der. Diferente do assert, ele sempre dispara, e recebe qualquer valor em vez de uma condição.

if hive.syslink.listen(me) is Result.Error(err) {
	panic err
}

O valor é renderizado exatamente como o echo o renderizaria, então panic err imprime a mensagem do erro e panic #Missing imprime Missing em vez de um número. Isso faz dele algo razoável de usar quando uma falha realmente significa que o programa não pode continuar.

Como ele nunca retorna, um ramo que termina em panic conta como caminho terminado — que é o assunto do próximo passo, e faz de panic "unreachable" um jeito legítimo de fechar uma cauda que você sabe ser inalcançável.

Uma exceção

Dentro de um serviço hive.syslink, um panic mata apenas aquele serviço e deixa o resto do programa rodando. O passo 138 explica por que isso é justamente o ponto.

55Every path must returnTodo caminho precisa retornar

A callable that returns something has to return it on every path. Hive checks this, and "falls off the end" is not a thing that can happen.

A path terminates by ending in return, in assert or panic, in an if/else where every branch terminates — or, and this is the useful one, in an else-less chain that covers its subject's whole type:

func describe(shape: Shape): Str {
	if shape is Shape.Circle(r) {
		return "circle, radius " + hive.conv.its(r)
	} else if shape is Shape.Rectangle(w, h) {
		return "rectangle, " + hive.conv.its(w) + "x" + hive.conv.its(h)
	} else if shape is Shape.Point {
		return "a single point"
	}
}

There is no else, and no return at the bottom. The three branches cover every variant Shape has, so the compiler can see that one of them must run. Delete any one of them and it refuses to compile — which means adding a fourth variant to Shape later turns every match on it into a compile error listing exactly the places that now need a decision.

The same holds for a Result, whose whole type is Ok plus Error. That is why so many snippets in this tour end with an else if … is Result.Error(_) and no final return.

Um callable que retorna algo tem que retornar em todo caminho. O Hive confere isso, e "cair pelo fim" não é uma coisa que possa acontecer.

Um caminho termina ao acabar em return, em assert ou panic, num if/else em que todo ramo termina — ou, e esta é a útil, numa cadeia sem else que cobre todo o tipo do assunto:

func describe(shape: Shape): Str {
	if shape is Shape.Circle(r) {
		return "circle, radius " + hive.conv.its(r)
	} else if shape is Shape.Rectangle(w, h) {
		return "rectangle, " + hive.conv.its(w) + "x" + hive.conv.its(h)
	} else if shape is Shape.Point {
		return "a single point"
	}
}

Não há else, e não há return no fim. Os três ramos cobrem toda variante que Shape tem, então o compilador consegue ver que um deles precisa rodar. Apague qualquer um e ele recusa a compilar — o que significa que adicionar uma quarta variante a Shape depois transforma todo casamento sobre ele em erro de compilação, listando exatamente os lugares que agora precisam de uma decisão.

O mesmo vale para um Result, cujo tipo inteiro é Ok mais Error. É por isso que tantos exemplos deste tour terminam com um else if … is Result.Error(_) e nenhum return final.

Part VIIParte VII

CallablesCallables

Two kinds of them, why the distinction is smaller than it looks, and how to treat one as a value.Dois tipos deles, por que a distinção é menor do que parece, e como tratar um como valor.

56proc and funcproc e func

You have been writing procs since step 3. A func is declared the same way, with the same parameter list and the same return type.

func greet(name: Str): Str {
	return "Hey {name}!"
}

proc main(): void {
	echo greet("Linus")
}
prints
Hey Linus!

Parameters are name: Type, the return type follows the colon after the parentheses, and void means the callable returns nothing.

The obvious guess about the difference — that a func is pure and a proc does I/O — is not what Hive means. Both may print, read files and talk to the network. The real difference is narrower, and the next step is all of it.

Você vem escrevendo procs desde o passo 3. Uma func é declarada do mesmo jeito, com a mesma lista de parâmetros e o mesmo tipo de retorno.

func greet(name: Str): Str {
	return "Hey {name}!"
}

proc main(): void {
	echo greet("Linus")
}
imprime
Hey Linus!

Parâmetros são nome: Tipo, o tipo de retorno vem depois dos dois-pontos após os parênteses, e void quer dizer que o callable não retorna nada.

O palpite óbvio sobre a diferença — de que uma func é pura e um proc faz I/O — não é o que o Hive quer dizer. Os dois podem imprimir, ler arquivos e falar com a rede. A diferença real é mais estreita, e o próximo passo é ela toda.

57Exactly two differencesExatamente duas diferenças

A func differs from a proc in two ways, and nothing else:

  • It cannot receive a mutex. A mut value passed to a func arrives as an ordinary immutable copy (step 21), so a func can never write into its caller's data.
  • It cannot call a proc. Only procs call procs.

Everything else — echo, reading a file with using, hive.net, hive.file — is allowed in either.

func summarise(path: Str): Str {
	if using path is Result.Ok(table) {      // I/O in a func: fine
		return "{len(table)} rows"
	}
	return "unreadable"
}

So the split is about reach, not purity: a func cannot affect anything its caller can see except through its return value, and cannot pull in the parts of the program that can. That is a smaller claim than purity, and it is one the compiler can actually check.

Write func by default and proc when you need one of the two things above. The examples that ship with the compiler follow that rule, and most of their helpers are funcs.

Uma func difere de um proc de duas formas, e mais nada:

  • Ela não pode receber um mutex. Um valor mut passado a uma func chega como cópia imutável comum (passo 21), então uma func nunca pode escrever nos dados de quem a chamou.
  • Ela não pode chamar um proc. Só procs chamam procs.

Todo o resto — echo, ler arquivo com using, hive.net, hive.file — é permitido nos dois.

func summarise(path: Str): Str {
	if using path is Result.Ok(table) {      // I/O numa func: ok
		return "{len(table)} rows"
	}
	return "unreadable"
}

Então a divisão é sobre alcance, não sobre pureza: uma func não pode afetar nada que quem a chamou veja, a não ser pelo valor de retorno, e não pode puxar as partes do programa que podem. É uma afirmação menor que pureza, e é uma que o compilador consegue de fato conferir.

Escreva func por padrão e proc quando você precisar de uma das duas coisas acima. Os exemplos que acompanham o compilador seguem essa regra, e a maioria dos helpers deles são funcs.

58Named argumentsArgumentos nomeados

Any argument may be passed by name, and this works for funcs, procs, queries and type constructors alike — including the built-in ones.

headers := [["Content-Type", "text/plain"]]
return hive.net.HttpResponse(200, body: "Hello!\n", headers: headers)

Named arguments may appear anywhere in the list; only the unnamed ones need to be in order, filling whichever parameters the named ones did not claim. Above, 200 lands on status because it is the only parameter left unclaimed before it.

Three rules: a name must exist, it may not repeat, and once you use named arguments the call has to cover the full parameter list.

Qualquer argumento pode ser passado por nome, e isso funciona para funcs, procs, queries e construtores de tipo igualmente — inclusive os embutidos.

headers := [["Content-Type", "text/plain"]]
return hive.net.HttpResponse(200, body: "Hello!\n", headers: headers)

Argumentos nomeados podem aparecer em qualquer posição da lista; só os sem nome precisam estar em ordem, preenchendo os parâmetros que os nomeados não reivindicaram. Acima, o 200 cai em status porque é o único parâmetro livre antes dele.

Três regras: um nome precisa existir, não pode repetir, e assim que você usa argumentos nomeados a chamada tem que cobrir a lista completa de parâmetros.

59A callable is a valueUm callable é um valor

Write a callable's name without parentheses and you get the callable itself, rather than a call to it.

proc main(): void {
	hive.net.httpServe(8080, handle)     // `handle` is passed, not called
}

proc handle(request: hive.net.HttpRequest): hive.net.HttpResponse {
	return hive.net.HttpResponse(200, [], "Hello!\n")
}

That is a bare reference, and it is how every handler in the networking library gets where it is going. The value can also be bound to a name and called later:

op := greet
echo op("Ada")

Escreva o nome de um callable sem parênteses e você recebe o próprio callable, em vez de uma chamada a ele.

proc main(): void {
	hive.net.httpServe(8080, handle)     // `handle` é passado, não chamado
}

proc handle(request: hive.net.HttpRequest): hive.net.HttpResponse {
	return hive.net.HttpResponse(200, [], "Hello!\n")
}

Essa é uma referência nua, e é assim que todo handler da biblioteca de rede chega onde precisa. O valor também pode ser vinculado a um nome e chamado depois:

op := greet
echo op("Ada")

60Writing down a function typeEscrevendo um tipo de função

If a callable can be a value, a parameter can ask for one. The type is written like a declaration with the name dropped.

func(Int): Int                                        // a pure one
proc(hive.net.HttpRequest): hive.net.HttpResponse      // an impure one

Which makes higher-order callables ordinary:

func twice(f: func(Int): Int, x: Int): Int {
	return f(f(x))
}

func addOne(n: Int): Int { return n + 1 }

proc main(): void {
	echo twice(addOne, 10)
}
prints
12

The type is usable as a parameter, a return type or a variable type — anywhere a type goes.

Se um callable pode ser um valor, um parâmetro pode pedir um. O tipo é escrito como uma declaração com o nome removido.

func(Int): Int                                        // um puro
proc(hive.net.HttpRequest): hive.net.HttpResponse      // um impuro

O que torna callables de ordem superior algo comum:

func twice(f: func(Int): Int, x: Int): Int {
	return f(f(x))
}

func addOne(n: Int): Int { return n + 1 }

proc main(): void {
	echo twice(addOne, 10)
}
imprime
12

O tipo serve como parâmetro, tipo de retorno ou tipo de variável — em qualquer lugar onde um tipo cabe.

61Partial application: a call with holesAplicação parcial: uma chamada com buracos

The second way to make a function value is to call a callable and leave some arguments blank, writing _ where a value would go.

func addN(n: Int, x: Int): Int { return n + x }

proc main(): void {
	increment := addN(1, _)      // a func(Int): Int
	echo increment(41)
}
prints
42

The arguments you supplied are fixed — captured by value, then and there — and each _ becomes a parameter of the resulting function, in order.

This is what adapts a callable to a slot that wants a different shape. A server wants a one-argument handler; your handler needs a database too:

hive.net.httpServe(8080, handler(_, db))

The two-argument handler becomes the one-argument handler httpServe expects, with db already inside it. Its declared shape is still checked at compile time, through the partial application.

O segundo jeito de fazer um valor de função é chamar um callable deixando alguns argumentos em branco, escrevendo _ onde iria um valor.

func addN(n: Int, x: Int): Int { return n + x }

proc main(): void {
	increment := addN(1, _)      // um func(Int): Int
	echo increment(41)
}
imprime
42

Os argumentos que você forneceu ficam fixos — capturados por valor, ali e naquele momento — e cada _ se torna um parâmetro da função resultante, em ordem.

É isso que adapta um callable a um slot que quer outra forma. Um servidor quer um handler de um argumento; o seu handler precisa também de um banco:

hive.net.httpServe(8080, handler(_, db))

O handler de dois argumentos se torna o handler de um argumento que o httpServe espera, com o db já dentro dele. A forma declarada continua sendo conferida em tempo de compilação, através da aplicação parcial.

62Pure widens to impurePuro se alarga para impuro

The func/proc split survives being turned into a value, and it travels in one direction.

  • A func value may be used where a proc is expected. Pure widens to impure — the slot promised less than it got.
  • A proc value may not fill a func slot.
  • A func still cannot call a proc value, even if one reaches it.

Which is the same rule as step 57, applied to values instead of declarations. Nothing new to remember.

A divisão func/proc sobrevive a ser transformada em valor, e ela viaja em uma direção só.

  • Um valor func pode ser usado onde se espera um proc. Puro se alarga para impuro — o slot prometeu menos do que recebeu.
  • Um valor proc não pode preencher um slot func.
  • Uma func ainda não pode chamar um valor proc, mesmo que um chegue até ela.

Que é a mesma regra do passo 57, aplicada a valores em vez de declarações. Nada novo para memorizar.

63Walking a vector: map, filter, filterMapCaminhando um vetor: map, filter, filterMap

Three builtins take a vector and a function over its elements, and each hands back a new vector. None of them touches the one they were given.

builtinsignaturegives you
mapmap(T[], func(T): K): K[dyn]every element, transformed
filterfilter(T[], func(T): Bool): T[dyn]the ones that passed
filterMapfilterMap(T[], func(T): Result<K, E>): K[dyn]transform and select at once
func label(n: Int): Str  { return "#{n}" }
func isEven(n: Int): Bool { return n % 2 == 0 }

proc main(): void {
	Int[dyn] nums = [1, 2, 3, 4]
	echo join(map(nums, label), " ")
	echo join(map(filter(nums, isEven), label), " ")
}
prints
#1 #2 #3 #4
#2 #4

filterMap is the one that earns its keep on input you do not trust. An Ok carries the element's new value; an Error says it has no place in the output. One pass, and no half-converted vector in between:

func asPort(s: Str): Result<Int, Bool> {
	if hive.conv.sti(s) is Result.Ok(n) && n > 0 && n < 65536 {
		return Result.Ok(n)
	}
	return Result.Error(false)
}

Str[dyn] raw = ["8080", "nope", "443"]
Int[dyn] ports = filterMap(raw, asPort)     // two of the three

The function must be a func, never a proc. A walk says nothing about the order it runs in or how often, so there is nowhere sensible to hang a side effect — and that is also exactly what lets a func body use one. To walk a vector with a proc, write a for each loop, which does say.

Each is specific about what its function answers with, and a mismatch is a Hive error rather than a puzzling one from further down: filter wants a Bool, filterMap a Result, and map wants something — a void function collects nothing, so that too is a for each loop.

A walk is sequential, always: one call per element, in order, on the calling thread. Nothing in map(urls, fetch) says how fetch runs, and a builtin is the last place a program should hide a decision like that. To run a batch of calls together, write the await-all (step 92) — which is also where the count of them lives.

bodies := map(urls, fetch)                          // one at a time, in order
Str[3] some = await [fetch(a), fetch(b), fetch(c)]   // all three at once

Três builtins recebem um vetor e uma função sobre os elementos dele, e cada um devolve um vetor novo. Nenhum deles toca o que recebeu.

builtinassinaturadevolve
mapmap(T[], func(T): K): K[dyn]todo elemento, transformado
filterfilter(T[], func(T): Bool): T[dyn]os que passaram
filterMapfilterMap(T[], func(T): Result<K, E>): K[dyn]transforma e seleciona de uma vez
func label(n: Int): Str  { return "#{n}" }
func isEven(n: Int): Bool { return n % 2 == 0 }

proc main(): void {
	Int[dyn] nums = [1, 2, 3, 4]
	echo join(map(nums, label), " ")
	echo join(map(filter(nums, isEven), label), " ")
}
imprime
#1 #2 #3 #4
#2 #4

O filterMap é o que se paga em entrada na qual você não confia. Um Ok carrega o novo valor do elemento; um Error diz que ele não tem lugar na saída. Uma passada, e nenhum vetor meio convertido no meio do caminho:

func asPort(s: Str): Result<Int, Bool> {
	if hive.conv.sti(s) is Result.Ok(n) && n > 0 && n < 65536 {
		return Result.Ok(n)
	}
	return Result.Error(false)
}

Str[dyn] raw = ["8080", "nope", "443"]
Int[dyn] ports = filterMap(raw, asPort)     // dois dos três

A função precisa ser uma func, nunca um proc. Uma caminhada não diz nada sobre a ordem em que roda nem quantas vezes, então não há onde pendurar um efeito colateral com sentido — e é exatamente isso que permite que um corpo de func use uma. Para caminhar um vetor com um proc, escreva um laço for each, que diz.

Cada um é específico sobre o que a função dele responde, e uma incompatibilidade é erro do Hive em vez de um erro confuso mais abaixo: o filter quer um Bool, o filterMap um Result, e o map quer alguma coisa — uma função void não coleta nada, então isso também é um laço for each.

Uma caminhada é sequencial, sempre: uma chamada por elemento, em ordem, na thread que chamou. Nada em map(urls, fetch) diz como o fetch roda, e um builtin é o último lugar onde um programa deveria esconder uma decisão dessas. Para rodar um lote de chamadas junto, escreva o await-all (passo 92) — que é também onde mora a contagem delas.

bodies := map(urls, fetch)                          // uma por vez, em ordem
Str[3] some = await [fetch(a), fetch(b), fetch(c)]   // as três de uma vez

64Putting a vector in order: sortColocando um vetor em ordem: sort

One argument orders elements by their own type's order. Like a walk, it hands back a new vector and leaves the one it was given alone.

proc main(): void {
	Str[3] words = ["pear", "apple", "fig"]

	echo join(sort(words), "|")
	echo join(words, "|")           // untouched
}
prints
apple|fig|pear
pear|apple|fig

Every type has an order except the ones there is no honest order for. Numbers ascend, a Str goes by code point, false comes before true, and an Atom goes by the value the compiler gave it. A vector is lexicographic, and on a shared prefix the shorter one comes first. A Result puts every Error before every Ok. A struct compares field by field in declaration order, and a tagged union compares by variant first — in the order the variants were declared, which is the one order you actually chose.

Two arguments order by a func of your own, which answers whether its first element comes before its second. That is also the answer for a type with no order of its own: anything can be sorted once you say how.

func longerFirst(a: Str, b: Str): Bool { return len(a) > len(b) }

echo join(sort(words, longerFirst), "|")     // apple|pear|fig

The sort is stable: elements neither of which comes first keep the order they arrived in. So the answer depends on the input alone, even when your ordering only looks at one field and says nothing about two rows that share it.

As with a walk, the function is a func, never a proc — a sort says nothing about how many comparisons it makes or in what order, so there is nowhere to hang a side effect. Nor is there anything for concurrency to overlap: a sort chooses each comparison from how the last one answered, so there is only ever one to make.

Com um argumento, ordena os elementos pela ordem do próprio tipo deles. Como uma caminhada, devolve um vetor novo e deixa em paz o que recebeu.

proc main(): void {
	Str[3] words = ["pear", "apple", "fig"]

	echo join(sort(words), "|")
	echo join(words, "|")           // intocado
}
imprime
apple|fig|pear
pear|apple|fig

Todo tipo tem uma ordem, menos aqueles para os quais não existe ordem honesta. Números sobem, uma Str vai por ponto de código, false vem antes de true, e um Atom vai pelo valor que o compilador deu a ele. Um vetor é lexicográfico, e num prefixo compartilhado o mais curto vem primeiro. Um Result põe todo Error antes de todo Ok. Um struct compara campo a campo na ordem de declaração, e uma união etiquetada compara primeiro pela variante — na ordem em que as variantes foram declaradas, que é a única ordem que você de fato escolheu.

Com dois argumentos, ordena por uma func sua, que responde se o primeiro elemento vem antes do segundo. Essa é também a resposta para um tipo sem ordem própria: qualquer coisa pode ser ordenada, desde que você diga como.

func longerFirst(a: Str, b: Str): Bool { return len(a) > len(b) }

echo join(sort(words, longerFirst), "|")     // apple|pear|fig

A ordenação é estável: elementos em que nenhum vem antes do outro mantêm a ordem em que chegaram. Então a resposta depende só da entrada, mesmo quando a sua ordem olha um campo só e não diz nada sobre duas linhas que o compartilham.

Como numa caminhada, a função é uma func, nunca um proc — uma ordenação não diz nada sobre quantas comparações faz nem em que ordem, então não há onde pendurar um efeito colateral. Nem há o que a concorrência possa sobrepor: uma ordenação escolhe cada comparação a partir de como a anterior respondeu, então só existe uma para fazer por vez.

65A discarded sort sorts in placeUm sort descartado ordena no lugar

Written as a statement, a sort throws away the vector it answers with. When the vector it was given is mut, that is taken at its word: the storage is reordered where it lies, with no copy at either end.

mut Str[dyn] names = ["pear", "apple"]

sort(names)                  // in place — `names` is now sorted
sorted := sort(names)        // NOT in place — a new vector, `names` untouched

Both conditions matter. Discarded, because a sort whose value you keep has to go on answering with a new vector — otherwise every b := sort(a) would quietly reorder a too. And mut, because storage an immutable binding can see is never rewritten underneath it; that is the invariant all of value semantics rests on.

A discarded sort that cannot sort in place is refused, rather than quietly doing nothing:

Int[dyn] v = [3, 1, 2]
sort(v)                  // rejected: `v` is not `mut`
sort(split(line, ","))   // rejected: nothing to sort in place

Both would sort a copy and drop it the moment it was made — dead code in the exact shape of the form that works. Declare the vector mut to sort it where it lies, or keep the answer.

Because sorting in place really is a write, a binding taken off that vector copies rather than sharing it: after b := a, a later sort(a) leaves b as it was. Two mut bindings still share completely, so sorting through either is seen through both.

Escrito como comando, um sort joga fora o vetor que ele devolve. Quando o vetor que ele recebeu é mut, isso é levado a sério: o armazenamento é reordenado onde está, sem cópia em nenhuma das pontas.

mut Str[dyn] names = ["pear", "apple"]

sort(names)                  // no lugar — `names` agora está ordenado
sorted := sort(names)        // NÃO no lugar — um vetor novo, `names` intocado

As duas condições importam. Descartado, porque um sort cujo valor você guarda tem de continuar devolvendo um vetor novo — senão todo b := sort(a) reordenaria a junto, caladamente. E mut, porque armazenamento que uma ligação imutável enxerga nunca é reescrito por baixo dela; é esse o invariante em que toda a semântica de valores se apoia.

Um sort descartado que não pode ordenar no lugar é recusado, em vez de silenciosamente não fazer nada:

Int[dyn] v = [3, 1, 2]
sort(v)                  // recusado: `v` não é `mut`
sort(split(line, ","))   // recusado: não há nada para ordenar no lugar

Os dois ordenariam uma cópia e a descartariam no instante em que ela fosse feita — código morto na forma exata da versão que funciona. Declare o vetor como mut para ordená-lo onde ele está, ou guarde a resposta.

Como ordenar no lugar é mesmo uma escrita, uma ligação tirada desse vetor copia em vez de compartilhar: depois de b := a, um sort(a) posterior deixa b como estava. Duas ligações mut continuam compartilhando por completo, então ordenar por qualquer uma é visto pelas duas.

66A known length survivesUm tamanho conhecido sobrevive

A length the compiler knows is not spent the first time you touch the vector. It carries through the operations that cannot lose it, so whole-vector code stays as indexable as the literals it started from.

Int[2] a = [2, 1]
Int[8] b = [1, 2, 3, 4, 5, 6, 7, 8]

both := a + b          // ten elements: the lengths add
echo both[9]           // no guard needed

doubled := map(a, twice)   // still two
ordered := sort(a)         // still two
echo ordered[1]            // no guard needed

Concatenation adds the two lengths. map preserves its input's — same length, same order, whoever the transform is — and sort does too, since a comparator decides where elements land, never how many there are. They compose, so map(a, f) + sort(b) is as long as a + b, and the result fills a declared Int[10] like any other vector of ten.

Two things it does not do. If either side of a concatenation has an unknown length the whole result is unknown — there is no "at least this many". And filter and filterMap never carry a length at all: what is knowable about them is a maximum, and a maximum can never put an index in range, because a filter that keeps nothing is always a possibility.

kept := filter(a, isEven)
echo kept[0]           // rejected: a filter may keep nothing

Um tamanho que o compilador conhece não se gasta na primeira vez que você toca o vetor. Ele atravessa as operações que não têm como perdê-lo, então código sobre o vetor inteiro continua tão indexável quanto os literais de onde partiu.

Int[2] a = [2, 1]
Int[8] b = [1, 2, 3, 4, 5, 6, 7, 8]

both := a + b          // dez elementos: os tamanhos se somam
echo both[9]           // sem precisar de guarda

doubled := map(a, twice)   // continua dois
ordered := sort(a)         // continua dois
echo ordered[1]            // sem precisar de guarda

A concatenação soma os dois tamanhos. O map preserva o da entrada — mesmo tamanho, mesma ordem, seja qual for a transformação — e o sort também, já que um comparador decide onde os elementos caem, nunca quantos são. Eles compõem, então map(a, f) + sort(b) tem o mesmo tamanho de a + b, e o resultado preenche um Int[10] declarado como qualquer outro vetor de dez.

Duas coisas que isso não faz. Se qualquer um dos lados de uma concatenação tiver tamanho desconhecido, o resultado inteiro é desconhecido — não existe "pelo menos tantos". E filter e filterMap nunca carregam tamanho nenhum: o que se sabe deles é um máximo, e um máximo nunca pode pôr um índice dentro da faixa, porque um filtro que não guarda nada é sempre uma possibilidade.

kept := filter(a, isEven)
echo kept[0]           // recusado: um filtro pode não guardar nada

67The whole set of builtinsO conjunto inteiro de builtins

You have now met all of them. They are always in scope, no import needed, and several are overloaded by argument type.

functionwhat it does
len(v) / len(s)elements in a vector / characters in a string
bytes(v) / bytes(s)footprint of a vector's storage / bytes of a string
append(v, x)grows a mut dynamic vector in place
prepend(v, x)the same, at the front
drop(v, low, high)removes an inclusive range from one, and hands it back
join(v, sep)Str vector into one string
split(s, sep)string into a Str vector
indexOf(v, x) / indexOf(s, sub)first position, as a Result
row(t, key) / column(t, key)look a table up by first cell / top cell
map / filter / filterMapwalk a vector into a new one
sort(v) / sort(v, first)the same elements, put in order

Sixteen names. That is the whole global surface of the language — everything else lives behind hive. and is the subject of Parts XIII and XIV.

Você já conheceu todos eles. Estão sempre em escopo, sem import, e vários são sobrecarregados pelo tipo do argumento.

funçãoo que faz
len(v) / len(s)elementos num vetor / caracteres numa string
bytes(v) / bytes(s)pegada do armazenamento de um vetor / bytes de uma string
append(v, x)cresce um vetor dinâmico mut no lugar
prepend(v, x)o mesmo, na frente
drop(v, low, high)remove uma faixa inclusiva de um deles, e a devolve
join(v, sep)vetor de Str em uma string
split(s, sep)string em um vetor de Str
indexOf(v, x) / indexOf(s, sub)primeira posição, como Result
row(t, k) / column(t, k)busca em tabela pela primeira célula / célula do topo
map / filter / filterMapcaminha um vetor para um novo
sort(v) / sort(v, first)os mesmos elementos, postos em ordem

Dezesseis nomes. Essa é a superfície global inteira da linguagem — todo o resto mora atrás de hive. e é assunto das Partes XIII e XIV.

68A declaration of your own winsUma declaração sua vence

If your program declares something named len, map or join, that is what your bare calls mean — with its own parameters and its own arity.

func len(label: Str, extra: Int): Str {   // ours
	return "{label}{extra}"
}

proc main(): void {
	Str[dyn] v = ["a", "b"]
	echo len("x", 2)          // ours: "x2"
	echo hive.len(v)          // the builtin: 2
	echo hive.join(v, "-")    // the long name always works
}

A name you declared quietly reading as somebody else's function is not a surprise a compiler should spring on you — and if builtins won, adding one would break every program that had already used the word. So the builtin yields, and stays reachable as hive.<name>.

Local bindings and parameters shadow the same way, and shadowing is per module: another file's declarations are only ever reached through its alias, so a map declared in one file leaves every other file's bare map alone.

Two consequences

Because the builtin is a distinct thing rather than a fallback, what it promises belongs to it alone. Only the builtin append requires a mut target. And only the builtin indexOf hands back an index the bounds pass accepts unguarded — a declared one promises nothing, so its result is guarded like any other integer.

Se o seu programa declara algo chamado len, map ou join, isso é o que as suas chamadas nuas significam — com os parâmetros dele e a aridade dele.

func len(label: Str, extra: Int): Str {   // o nosso
	return "{label}{extra}"
}

proc main(): void {
	Str[dyn] v = ["a", "b"]
	echo len("x", 2)          // o nosso: "x2"
	echo hive.len(v)          // o builtin: 2
	echo hive.join(v, "-")    // o nome longo sempre funciona
}

Um nome que você declarou passar a significar silenciosamente a função de outra pessoa não é uma surpresa que um compilador deva pregar em você — e, se os builtins vencessem, adicionar um quebraria todo programa que já tivesse usado a palavra. Então o builtin cede, e continua alcançável como hive.<nome>.

Vínculos locais e parâmetros sombreiam do mesmo jeito, e o sombreamento é por módulo: um map declarado num arquivo deixa o map puro de todo outro arquivo em paz.

Duas consequências

Como o builtin é uma coisa distinta e não um fallback, o que ele promete pertence só a ele. Só o append builtin exige um alvo mut. E só o indexOf builtin devolve um índice que a passagem de limites aceita sem guarda — um declarado não promete nada, então o resultado dele é guardado como qualquer outro inteiro.

Part VIIIParte VIII

Types of your ownTipos seus

One declaration form that gives you either a record or a closed set of alternatives.Uma forma de declaração que dá a você ou um registro ou um conjunto fechado de alternativas.

69No variants: a structSem variantes: um struct

type with a list of fields gives you a record.

type Item {
	name: Str
	pence: Int
}

proc main(): void {
	tool := Item("Hive tool", 899)
	echo tool.name
	echo tool.pence
}
prints
Hive tool
899

Construct one by calling the type's name with its fields in declared order, or by name: Item(name: "Hive tool", pence: 899). Read a field with a dot.

A field is writable if the value holding it is mut:

mut basket := Basket(items: [1, 2, 3])
basket.items = [4, 5]

type com uma lista de campos dá a você um registro.

type Item {
	name: Str
	pence: Int
}

proc main(): void {
	tool := Item("Hive tool", 899)
	echo tool.name
	echo tool.pence
}
imprime
Hive tool
899

Construa um chamando o nome do tipo com os campos na ordem declarada, ou por nome: Item(name: "Hive tool", pence: 899). Leia um campo com um ponto.

Um campo é escrevível se o valor que o contém é mut:

mut basket := Basket(items: [1, 2, 3])
basket.items = [4, 5]

70Variants: a tagged unionCom variantes: uma união etiquetada

Give a type variants instead and you get a closed set of alternatives. Each variant has its own fields, and a value of the type is exactly one of them.

type Shape {
	Circle {
		radius: Int
	}
	Rectangle {
		width: Int
		height: Int
	}
	Point
}

A variant with no fields — Point — needs no braces. Variants are reached through the type name, and constructed the same way a struct is:

a := Shape.Circle(5)
b := Shape.Rectangle(3, 4)
c := Shape.Point()

"Closed" is the important word. Shape has three variants and cannot grow one at a distance, which is what lets the compiler check in step 55 that a chain of branches covered them all.

Getting the fields back out is is, in Part IX.

Dê ao tipo variantes em vez disso e você recebe um conjunto fechado de alternativas. Cada variante tem os campos dela, e um valor do tipo é exatamente uma delas.

type Shape {
	Circle {
		radius: Int
	}
	Rectangle {
		width: Int
		height: Int
	}
	Point
}

Uma variante sem campos — Point — não precisa de chaves. Variantes são alcançadas pelo nome do tipo, e construídas do mesmo jeito que um struct:

a := Shape.Circle(5)
b := Shape.Rectangle(3, 4)
c := Shape.Point()

"Fechado" é a palavra importante. Shape tem três variantes e não pode ganhar uma à distância, que é o que permite ao compilador conferir, no passo 55, que uma cadeia de ramos cobriu todas elas.

Tirar os campos de volta é o is, na Parte IX.

71Fields outside every variantCampos fora de toda variante

A field declared in the type but outside any variant is added to every variant.

type ParsingResult {
	Success {
		headerlessTable: Table
	}
	NoData
	Error {
		error: hive.TableError
	}

	// Every variant above gets this too.
	timestamp: Int
}

So a Success carries a table and a timestamp, and a NoData carries just a timestamp. When constructing, the variant's own fields come first and the shared ones follow:

return ParsingResult.Success(table[1:], hive.time.now())
return ParsingResult.NoData(hive.time.now())

It saves repeating a field that every case genuinely has — a timestamp, a request id, the input that was being processed.

Um campo declarado no tipo mas fora de qualquer variante é adicionado a todas as variantes.

type ParsingResult {
	Success {
		headerlessTable: Table
	}
	NoData
	Error {
		error: hive.TableError
	}

	// Toda variante acima recebe isto também.
	timestamp: Int
}

Então um Success carrega uma tabela e um timestamp, e um NoData carrega só um timestamp. Ao construir, os campos próprios da variante vêm primeiro e os compartilhados depois:

return ParsingResult.Success(table[1:], hive.time.now())
return ParsingResult.NoData(hive.time.now())

Isso poupa repetir um campo que todo caso realmente tem — um timestamp, um id de requisição, a entrada que estava sendo processada.

72Result<T, E>, the one you already useResult<T, E>, o que você já usa

Result is a tagged union with two variants: Ok carrying a value, and Error carrying a reason. Every fallible operation in the language returns one — indexOf, using, every parse, every network call.

func asPort(s: Str): Result<Int, Bool> {
	if hive.conv.sti(s) is Result.Ok(n) && n > 0 && n < 65536 {
		return Result.Ok(n)
	}
	return Result.Error(false)
}

It is spelled with its two type arguments — the success type and the error type — and it is an ordinary type you can write down anywhere, including in a parameter:

proc report(label: Str, result: Result<User[dyn], hive.sql.SqlError>): void {
	if result is Result.Error(error) {
		echo "{label}: {error.reason}: {error.message}"
		return
	}
	// ...
}

There is no exception mechanism in Hive, and nothing to catch. A failure is a value with a type, and the compiler's insistence that you cover both variants (step 55) is what stops one being ignored.

Notice too that Result being a two-variant union is why an Ok branch followed by an Error branch counts as exhaustive. It is not a special case in the compiler; it falls out of step 67.

Result é uma união etiquetada com duas variantes: Ok, carregando um valor, e Error, carregando um motivo. Toda operação que pode falhar na linguagem retorna uma — indexOf, using, toda conversão, toda chamada de rede.

func asPort(s: Str): Result<Int, Bool> {
	if hive.conv.sti(s) is Result.Ok(n) && n > 0 && n < 65536 {
		return Result.Ok(n)
	}
	return Result.Error(false)
}

Ele é escrito com seus dois argumentos de tipo — o tipo do sucesso e o tipo do erro — e é um tipo comum que você pode escrever em qualquer lugar, inclusive num parâmetro:

proc report(label: Str, result: Result<User[dyn], hive.sql.SqlError>): void {
	if result is Result.Error(error) {
		echo "{label}: {error.reason}: {error.message}"
		return
	}
	// ...
}

Não existe mecanismo de exceção no Hive, e nada para capturar. Uma falha é um valor com um tipo, e a insistência do compilador em que você cubra as duas variantes (passo 55) é o que impede que uma seja ignorada.

Repare também que o Result ser uma união de duas variantes é por que um ramo Ok seguido de um ramo Error conta como exaustivo. Não é um caso especial no compilador; isso cai do passo 67.

73Custom types are the JSON schema, tooTipos próprios também são o schema JSON

One thing worth flagging early, because it changes how you design types: a Hive type declaration is a schema. The JSON library derives an encoder and a decoder for any type at compile time, and the distribution library derives the wire format from the same declaration.

type Session {
	user: Str
	role: Str
	exp: Int
}

// Sign a JWT whose payload is this type; verify it straight back into one.
token := hive.crypto.jwtSign(Session("ada", "admin", hive.time.now() + 3600), secret)

So there is no separate schema language, no annotations, and no mapping layer to keep in step. Part XIV picks this up properly; for now, know that a type you declare for your own use is already the thing the rest of the library needs.

Uma coisa que vale sinalizar cedo, porque muda como você projeta tipos: uma declaração de tipo no Hive é um schema. A biblioteca de JSON deriva um codificador e um decodificador para qualquer tipo em tempo de compilação, e a biblioteca de distribuição deriva o formato de rede da mesma declaração.

type Session {
	user: Str
	role: Str
	exp: Int
}

// Assina um JWT cujo payload é este tipo; verifica de volta para um deles.
token := hive.crypto.jwtSign(Session("ada", "admin", hive.time.now() + 3600), secret)

Então não há linguagem de schema separada, nem anotações, nem camada de mapeamento para manter em sincronia. A Parte XIV retoma isso direito; por ora, saiba que um tipo que você declara para uso próprio já é a coisa de que o resto da biblioteca precisa.

Part IXParte IX

Pattern matchingCasamento de padrões

One operator, is, that checks a value against a shape and binds the pieces as it goes.Um operador, is, que confere um valor contra uma forma e vincula as partes no caminho.

74Asking what a value isPerguntando o que um valor é

is tests a value against a pattern and produces a Bool. Because it is just an expression, it goes wherever a condition goes.

if shape is Shape.Point {
	echo "a single point"
}

Hive matches four kinds of thing with is: the variants of a tagged union, a Result, a vector, and a string. This part takes them one at a time.

Note that there is no match or switch statement. A chain of if/else if is how you cover several cases, and step 55 is what makes that safe: cover a type's whole set of variants and the compiler knows the chain is exhaustive.

is testa um valor contra um padrão e produz um Bool. Como é apenas uma expressão, ele cabe em qualquer lugar onde caiba uma condição.

if shape is Shape.Point {
	echo "a single point"
}

O Hive casa quatro tipos de coisa com is: as variantes de uma união etiquetada, um Result, um vetor e uma string. Esta parte trata de uma por vez.

Repare que não existe match nem switch. Uma cadeia de if/else if é como você cobre vários casos, e o passo 55 é o que torna isso seguro: cubra todas as variantes de um tipo e o compilador sabe que a cadeia é exaustiva.

75Binding a variant's fieldsVinculando os campos de uma variante

Put names in parentheses after the variant and its fields are bound to them, by position, for the whole branch.

func describe(shape: Shape): Str {
	if shape is Shape.Circle(r) {
		return "circle, radius " + hive.conv.its(r)
	} else if shape is Shape.Rectangle(w, h) {
		return "rectangle, " + hive.conv.its(w) + "x" + hive.conv.its(h)
	} else if shape is Shape.Point {
		return "a single point"
	}
}
prints
circle, radius 5
rectangle, 3x4
a single point

r, w and h are ordinary immutable names that exist only inside their branch. _ in a position matches the field without binding anything — useful when you care that a variant matched but not what it carries.

Coloque nomes entre parênteses depois da variante e os campos dela são vinculados a esses nomes, por posição, para todo o bloco.

func describe(shape: Shape): Str {
	if shape is Shape.Circle(r) {
		return "circle, radius " + hive.conv.its(r)
	} else if shape is Shape.Rectangle(w, h) {
		return "rectangle, " + hive.conv.its(w) + "x" + hive.conv.its(h)
	} else if shape is Shape.Point {
		return "a single point"
	}
}
imprime
circle, radius 5
rectangle, 3x4
a single point

r, w e h são nomes imutáveis comuns que só existem dentro do seu bloco. Um _ numa posição casa com o campo sem vincular nada — útil quando importa que a variante casou, mas não o que ela carrega.

76A binding works immediatelyO vínculo já vale na mesma condição

A name bound by is is usable in the rest of the same condition, after an &&. This is where step 48's short-circuiting earns its keep.

if example is CustomType.Example2(something) && something == "Example" {
	return something
}

Read it left to right: if this is an Example2, call its field something, and if that equals "Example"…. The second test only runs when the first succeeded, so something is guaranteed to exist by the time it is read.

The same trick works on a call, and the call is evaluated exactly once:

if hive.conv.sti(s) is Result.Ok(n) && n > 0 && n < 65536 {
	return Result.Ok(n)
}

This one line replaces the usual parse-then-check-then-unwrap dance, and there is no state in between where n exists but has not been validated.

Um nome vinculado por is já pode ser usado no resto da mesma condição, depois de um &&. É aqui que o curto-circuito do passo 48 se paga.

if example is CustomType.Example2(something) && something == "Example" {
	return something
}

Leia da esquerda para a direita: se isto é um Example2, chame o campo dele de something, e se isso for igual a "Example"…. O segundo teste só roda se o primeiro deu certo, então something com certeza existe quando é lido.

O mesmo truque funciona sobre uma chamada, e a chamada é avaliada exatamente uma vez:

if hive.conv.sti(s) is Result.Ok(n) && n > 0 && n < 65536 {
	return Result.Ok(n)
}

Essa linha substitui a dancinha de converter, depois checar, depois desempacotar — e não existe nenhum momento no meio em que n exista sem ter sido validado.

77Matching a ResultCasando um Result

A Result is a tagged union, so it matches like any other — but it comes up often enough to be worth its own step.

func parseAge(text: Str): Str {
	parsed := hive.conv.sti(text)
	if parsed is Result.Ok(age) {
		return text + " -> age " + hive.conv.its(age)
	} else if parsed is Result.Error(problem) {
		return text + " -> " + problem.message
	}
}
prints
42 -> age 42
twelve -> not a valid integer

Ok binds the value; Error binds the reason, which is a value with fields of its own — here a message, and for most errors in the library also a short reason tag you can branch on.

Both branches, and no else: two variants is the whole type, so the function returns on every path.

Um Result é uma união etiquetada, então casa como qualquer outra — mas aparece com frequência suficiente para merecer o próprio passo.

func parseAge(text: Str): Str {
	parsed := hive.conv.sti(text)
	if parsed is Result.Ok(age) {
		return text + " -> age " + hive.conv.its(age)
	} else if parsed is Result.Error(problem) {
		return text + " -> " + problem.message
	}
}
imprime
42 -> age 42
twelve -> not a valid integer

Ok vincula o valor; Error vincula o motivo, que é um valor com campos próprios — aqui um message, e na maior parte dos erros da biblioteca também um reason curto sobre o qual você pode ramificar.

Os dois blocos, e nenhum else: duas variantes são o tipo inteiro, então a função retorna em todo caminho.

78Vector patternsPadrões de vetor

A vector pattern matches positionally, and a pattern without a tail matches only a vector of exactly that length.

if command is ["stop"] {
	return "halt"
} else if command is [single] {
	return "one-word command: " + single
}

Each position is one of three things: a literal to compare against ("stop", 3, #Atom), a name to bind that element to, or _ to skip it. So ["stop"] means "exactly one element, equal to stop", and [single] means "exactly one element, whatever it is".

This is also a tidy way to say "exactly one row came back", which you will meet again in the SQL part:

if using node.db run lookup(name) is Result.Ok(values) && values is [value] {
	// exactly one match, and it is bound to `value`
}

Um padrão de vetor casa por posição, e um padrão sem cauda só casa com um vetor de exatamente aquele tamanho.

if command is ["stop"] {
	return "halt"
} else if command is [single] {
	return "one-word command: " + single
}

Cada posição é uma de três coisas: um literal para comparar ("stop", 3, #Atom), um nome para vincular aquele elemento, ou _ para pular. Então ["stop"] quer dizer "exatamente um elemento, igual a stop", e [single] quer dizer "exatamente um elemento, qualquer que seja".

Isso também é um jeito limpo de dizer "veio exatamente uma linha", coisa que reaparece na parte de SQL:

if using node.db run lookup(name) is Result.Ok(values) && values is [value] {
	// exactly one match, and it is bound to `value`
}

79A tail: ...restUma cauda: ...rest

A trailing ...name relaxes the length from an exact count to a minimum, and binds the leftover elements as a vector.

func route(command: Str[dyn]): Str {
	// `direction` binds command[1]; `steps` binds everything after it.
	if command is ["move", direction, ...steps] {
		return "move " + direction + " (" + hive.conv.its(len(steps)) + " extra arg(s))"
	}
	return "unrecognised command"
}
prints
move north (2 extra arg(s))

That pattern reads: at least two elements, the first being "move". The tail may be empty, so a bare ["move", direction] matches it too, with steps bound to an empty vector.

Because the tail is a vector like any other, len(steps) works on it — and because its length is unknown, indexing into it needs a guard.

Um ...nome no fim afrouxa o tamanho de uma contagem exata para um mínimo, e vincula os elementos restantes como um vetor.

func route(command: Str[dyn]): Str {
	// `direction` vincula command[1]; `steps` vincula tudo depois disso.
	if command is ["move", direction, ...steps] {
		return "move " + direction + " (" + hive.conv.its(len(steps)) + " extra arg(s))"
	}
	return "unrecognised command"
}
imprime
move north (2 extra arg(s))

Esse padrão se lê: ao menos dois elementos, sendo o primeiro "move". A cauda pode ser vazia, então um ["move", direction] puro também casa, com steps vinculado a um vetor vazio.

Como a cauda é um vetor como qualquer outro, len(steps) funciona nela — e como o tamanho dela é desconhecido, indexá-la exige uma guarda.

80String patternsPadrões de string

This is the one that most often surprises people, and it is the reason step 10 could take away s[0] without taking anything useful with it.

A string pattern is a template: literal text that has to match verbatim, plus {name} holes that bind whatever spans them.

func handle(path: Str): Str {
	if path is "/health" {
		return "200 health check"
	} else if path is "/users/{id}/posts/{postId}" {
		return "user " + id + ", post " + postId
	} else if path is "/users/{id}" {
		return "user " + id
	} else if path is "/files/{rest}" {
		return "file at " + rest
	}
	return "404 not found"
}
prints
200 health check
user 7, post 99
user 7
file at img/logo.png
404 not found

Four things are going on there, and each is worth naming:

  • The template must cover the whole string — this is a match, not a search.
  • A pattern with no holes ("/health") is simply an exact comparison.
  • Matching is non-greedy, so {id} sitting between two slashes never swallows a slash. That is why "/users/{id}/posts/{postId}" works.
  • A hole with no literal after it runs to the end, slashes included — which is why "/files/{rest}" caught img/logo.png whole.

Holes may sit in the middle of the template, not just at the end. That is what makes this a routing tool rather than a prefix check.

Este é o que mais surpreende, e é a razão pela qual o passo 10 pôde tirar o s[0] sem levar nada de útil junto.

Um padrão de string é um gabarito: texto literal que precisa casar exatamente, mais buracos {nome} que vinculam o que estiver entre eles.

func handle(path: Str): Str {
	if path is "/health" {
		return "200 health check"
	} else if path is "/users/{id}/posts/{postId}" {
		return "user " + id + ", post " + postId
	} else if path is "/users/{id}" {
		return "user " + id
	} else if path is "/files/{rest}" {
		return "file at " + rest
	}
	return "404 not found"
}
imprime
200 health check
user 7, post 99
user 7
file at img/logo.png
404 not found

Quatro coisas acontecem aí, e cada uma merece nome:

  • O gabarito precisa cobrir a string inteira — isto é um casamento, não uma busca.
  • Um padrão sem buracos ("/health") é só uma comparação exata.
  • O casamento é não-guloso, então um {id} entre duas barras nunca engole uma barra. É por isso que "/users/{id}/posts/{postId}" funciona.
  • Um buraco sem literal depois dele vai até o fim, barras inclusive — foi assim que "/files/{rest}" pegou img/logo.png inteiro.

Os buracos podem ficar no meio do gabarito, não só no fim. É isso que faz disto uma ferramenta de rotas, e não uma checagem de prefixo.

81What a string pattern refusesO que um padrão de string recusa

Two restrictions, both compile errors, both for the same reason: the pattern has to have one unambiguous reading.

does not compile
if path is "/users/{id}{name}" { }

Two holes side by side. There is no literal text between them, so there is no way to decide where one ends and the next begins.

And a hole has to be a plain binding name — {a.b}, {len(x)} or anything else that looks like an expression is refused. A pattern binds; it does not compute.

Every form in this part, in one runnable program: code-examples/7 - Pattern Matching/

Duas restrições, as duas erros de compilação, as duas pelo mesmo motivo: o padrão precisa ter uma leitura só.

não compila
if path is "/users/{id}{name}" { }

Dois buracos lado a lado. Não há texto literal entre eles, então não há como decidir onde um termina e o outro começa.

E um buraco tem que ser um nome simples — {a.b}, {len(x)} ou qualquer outra coisa com cara de expressão é recusado. Um padrão vincula; ele não calcula.

Todas as formas desta parte, num programa que roda: code-examples/7 - Pattern Matching/
Part XParte X

GenericsGenéricos

Write a callable once for every element type — and pay nothing at runtime for it.Escreva um callable uma vez para todo tipo de elemento — e não pague nada por isso em tempo de execução.

82A type variableUma variável de tipo

A name in a signature that is neither a builtin type nor one you declared is a type variable, and it makes the callable generic in it. There is no <T> list to write first — using the name is the declaration.

func first(v: T[]): Result<T, Bool> {
	if len(v) > 0 {
		return Result.Ok(v[0])
	}
	return Result.Error(false)
}

Which reads: a vector of some element type, and a Result carrying that same type. Call it with whatever you like and the types are read off the arguments:

Str[2]   names  = ["ada", "grace"]
Int[dyn] counts = [7, 8, 9]

if first(names) is Result.Ok(n) { echo n }      // n is a Str
if first(counts) is Result.Ok(c) { echo c }     // c is an Int
prints
ada
7

You have been reading this notation since step 64: len(T[]): Int and indexOf(T[], T): Result<Int, Bool> are how the builtins were described. The same notation is now available to your own code.

Um nome numa assinatura que não é um tipo embutido nem um tipo que você declarou é uma variável de tipo, e ela torna o callable genérico nela. Não existe lista <T> para escrever antes — usar o nome já é a declaração.

func first(v: T[]): Result<T, Bool> {
	if len(v) > 0 {
		return Result.Ok(v[0])
	}
	return Result.Error(false)
}

Que se lê: um vetor de algum tipo de elemento, e um Result carregando esse mesmo tipo. Chame com o que quiser e os tipos são lidos dos argumentos:

Str[2]   names  = ["ada", "grace"]
Int[dyn] counts = [7, 8, 9]

if first(names) is Result.Ok(n) { echo n }      // n é um Str
if first(counts) is Result.Ok(c) { echo c }     // c é um Int
imprime
ada
7

Você já vem lendo essa notação desde o passo 64: len(T[]): Int e indexOf(T[], T): Result<Int, Bool> foi como os builtins foram descritos. Agora a mesma notação está disponível para o seu código.

83One copy per set of type argumentsUma cópia por conjunto de tipos

Nothing about this is dynamic. Every call site is resolved at compile time, the type arguments are read off the argument types, and one concrete copy is emitted per distinct set of them. No boxing, no dispatch, no reflection.

you writefunc first(v: T[]): Result<T, Bool>
the compiler emits
first_Strfirst(names) — Str[2]
first_Intfirst(counts) — Int[dyn]
first_Boolnever called, never emitted

Because each copy is an ordinary declaration, every check runs on it separately — and two of them get sharper for it. An instantiation at Str[3] is held to that length, while one at Str[dyn] guards its indexes. That is the right answer for both, and it is not something a single shared implementation could give you.

The body may write the type variables down too, and each copy substitutes through its own body:

mut K[dyn] newValues = []
for each value: T in values { }

Nada disso é dinâmico. Todo ponto de chamada é resolvido em tempo de compilação, os tipos são lidos dos argumentos, e uma cópia concreta é gerada por conjunto distinto deles. Sem boxing, sem despacho, sem reflexão.

você escrevefunc first(v: T[]): Result<T, Bool>
o compilador gera
first_Strfirst(names) — Str[2]
first_Intfirst(counts) — Int[dyn]
first_Boolnunca chamado, nunca gerado

Como cada cópia é uma declaração comum, cada verificação roda nela separadamente — e duas delas ficam mais afiadas por causa disso. Uma instanciação em Str[3] é cobrada por aquele tamanho, enquanto uma em Str[dyn] guarda seus índices. Essa é a resposta certa para as duas, e não é algo que uma implementação única compartilhada conseguiria dar.

O corpo também pode escrever as variáveis de tipo, e cada cópia substitui no próprio corpo:

mut K[dyn] newValues = []
for each value: T in values { }

84Inferred from the parameters — including inside themInferido dos parâmetros — inclusive dentro deles

A variable is pinned down by wherever it appears in the parameters, and that includes inside a parameter's own type. Which is exactly what makes a higher-order generic work.

func filterMap(values: T[], transform: func(T): Result<K, E>): K[dyn] {
	mut K[dyn] newValues = []
	for each value in values {
		if transform(value) is Result.Ok(newValue) {
			append(newValues, newValue)
		}
	}
	return newValues
}

Three variables, none of them written at the call site: the vector says what T is, and the function says what K and E are.

Str[dyn] raw = ["8080", "nope", "443"]
Int[dyn] ports = filterMap(raw, asPort)     // T=Str, K=Int, E=Bool

Note the return is written K[dyn], and it has to say which kind it is. [] is a parameter spelling: there it lets one helper serve a caller holding a Str[3] and one holding a Str[dyn] alike. A return is the other way round — it is where the caller is told what it is getting, and guarding every index is a different answer from indexing freely.

Uma variável é fixada por onde ela aparece nos parâmetros, e isso inclui dentro do tipo de um parâmetro. É exatamente isso que faz um genérico de ordem superior funcionar.

func filterMap(values: T[], transform: func(T): Result<K, E>): K[dyn] {
	mut K[dyn] newValues = []
	for each value in values {
		if transform(value) is Result.Ok(newValue) {
			append(newValues, newValue)
		}
	}
	return newValues
}

Três variáveis, nenhuma escrita no ponto de chamada: o vetor diz o que é T, e a função diz o que são K e E.

Str[dyn] raw = ["8080", "nope", "443"]
Int[dyn] ports = filterMap(raw, asPort)     // T=Str, K=Int, E=Bool

Repare que o retorno é escrito K[dyn], e ele precisa dizer de qual tipo é. [] é uma grafia de parâmetro: ali ela deixa um mesmo auxiliar atender tanto quem tem um Str[3] quanto quem tem um Str[dyn]. Um retorno é o contrário — é onde o chamador é informado do que está recebendo, e proteger todo índice é uma resposta diferente de indexar à vontade.

85Generic typesTipos genéricos

A type whose fields mention variables is generic the same way — and because a type is written down where it is used, its arguments are written out there.

type Box {
	items: T[dyn]
	label: Str
}

type Either {
	Left  { left: A }
	Right { right: B }
}

proc main(): void {
	Box<Str> people  = Box(["ada", "grace"], "people")
	Box<Int> tallies = Box([1, 2, 3], "tallies")

	Either<Str, Int> answer = Either.Right(42)
	if answer is Either.Right(n) { echo n }

	echo hive.json.encode(people)
}
prints
42
{"items":["ada","grace"],"label":"people"}

The variables are ordered by first appearance, so Either<Str, Int> means A is Str and B is Int. Each instantiation is a genuinely separate type all the way down: its own copying code, its own JSON codec, its own wire identity. Either<Str, Int> and Either<Int, Str> are not interchangeable.

Um tipo cujos campos mencionam variáveis é genérico do mesmo jeito — e como um tipo é escrito onde é usado, os argumentos dele são escritos ali.

type Box {
	items: T[dyn]
	label: Str
}

type Either {
	Left  { left: A }
	Right { right: B }
}

proc main(): void {
	Box<Str> people  = Box(["ada", "grace"], "people")
	Box<Int> tallies = Box([1, 2, 3], "tallies")

	Either<Str, Int> answer = Either.Right(42)
	if answer is Either.Right(n) { echo n }

	echo hive.json.encode(people)
}
imprime
42
{"items":["ada","grace"],"label":"people"}

As variáveis são ordenadas por primeira aparição, então Either<Str, Int> quer dizer que A é Str e B é Int. Cada instanciação é um tipo realmente separado até o fim: código de cópia próprio, codec JSON próprio, identidade própria na rede. Either<Str, Int> e Either<Int, Str> não são intercambiáveis.

86What generics refuseO que os genéricos recusam

Two refusals, and both follow from step 80 rather than being extra rules.

does not compile
f := first

A generic callable cannot be used as a value. Which copy a call reaches is decided by the argument types, and a value carries none — there is no way to say which first this is.

does not compile
func makeOne(n: Int): T { ... }
echo makeOne(3)                 // the error lands here

A variable that appears only in the return type has nothing to be inferred from. The declaration alone is fine — nothing has been asked of T yet — and the call is where it comes apart: makeOne(3) says nothing about which copy is wanted, so that is where the compiler says so.

And a generic that instantiates itself at an ever-larger type would never settle, so the expansion is capped: overrunning the cap is a compile error rather than a build that never finishes.

Full tour, including the rejected cases: code-examples/14 - Generics/

Duas recusas, e as duas seguem do passo 80 em vez de serem regras extras.

não compila
f := first

Um callable genérico não pode ser usado como valor. Qual cópia uma chamada alcança é decidido pelos tipos dos argumentos, e um valor não carrega nenhum — não há como dizer qual first é este.

não compila
func makeOne(n: Int): T { ... }
echo makeOne(3)                 // o erro cai aqui

Uma variável que aparece só no tipo de retorno não tem de onde ser inferida. A declaração sozinha passa — ainda não se pediu nada de T — e é na chamada que a coisa desanda: makeOne(3) não diz qual cópia se quer, então é ali que o compilador avisa.

E um genérico que se instancia num tipo cada vez maior nunca se acomodaria, então a expansão tem um teto: passar do teto é erro de compilação, e não um build que nunca termina.

Tour completo, incluindo os casos recusados: code-examples/14 - Generics/
Part XIParte XI

ConcurrencyConcorrência

Nothing on the declaration, and then the call site decides everything.Nada na declaração, e daí o ponto de chamada decide todo o resto.

87Every call blocksToda chamada bloqueia

Start from the rule, because everything in this part follows from it: a call blocks the thread that made it, and waits for its value. Always.

func slowShout(text: Str): Str {
	hive.task.sleep(1000)
	return text + "!!!"
}

Notice what the declaration says about concurrency: nothing at all. It is an ordinary func. There is no async func, no Future, no Promise, no executor to configure, and nothing dynamically typed — because the declaration is the wrong place to decide any of it. Whoever calls a function is the one who knows whether they need its answer.

So the whole of Hive's concurrency lives at the call site, in four shapes the next steps take one at a time.

Comece pela regra, porque tudo nesta parte decorre dela: uma chamada bloqueia a thread que a fez, e espera pelo valor dela. Sempre.

func slowShout(text: Str): Str {
	hive.task.sleep(1000)
	return text + "!!!"
}

Repare no que a declaração diz sobre concorrência: absolutamente nada. É uma func comum. Não existe async func, não há Future, não há Promise, não há executor para configurar, e nada de tipagem dinâmica — porque a declaração é o lugar errado para decidir qualquer disso. Quem chama uma função é quem sabe se precisa da resposta dela.

Então toda a concorrência do Hive vive no ponto de chamada, em quatro formas que os próximos passos veem uma por uma.

88Waiting for one: just call itEsperando por uma: só chame

The first shape is the one you already know how to write.

echo slowShout("waited for")
prints (a second later)
waited for!!!

Its type is the func's return type — a Str here — with nothing wrapped around it. And it costs nothing to arrange: no virtual thread is created, no channel is allocated, nothing is scheduled. It is a function call.

That is worth stating because in a language with an async keyword you might expect the plain form to be the special one. Here it is the other way round: waiting is the default, and you pay only for not waiting.

A primeira forma é a que você já sabe escrever.

echo slowShout("waited for")
imprime (um segundo depois)
waited for!!!

O tipo dela é o tipo de retorno da func — um Str, aqui — sem nada embrulhado em volta. E não custa nada para montar: nenhuma thread virtual é criada, nenhum canal é alocado, nada é agendado. É uma chamada de função.

Vale dizer isso porque, numa linguagem com uma palavra async, você poderia esperar que a forma pura fosse a especial. Aqui é o contrário: esperar é o padrão, e você paga apenas por não esperar.

89async: fire and forgetasync: dispare e esqueça

Put async in front of a call and it runs on its own virtual thread — a thread the runtime multiplexes onto real ones, cheap enough to have thousands of — while the caller carries straight on.

proc main(): void {
	async slowShout("logged in the background")
	echo "main did not wait"
}
prints
main did not wait

Fire-and-forget is the whole of what this is. Written as a statement the call's result is discarded, and nothing is left behind to read one from. Give the same call a name and it keeps its result instead — that is step 91, and the only other place async may be written. Inside a larger expression it may not: echo len(async f(x)) is a compile error, because there the value is wanted on the spot, which is what a plain call already does.

It works on any call — a func, a proc, a query, the standard library, a service (Part XV). This is how you start a server that blocks forever without blocking the program that started it, which you will see in Part XIV.

does not compile
async len(v)

The one group of calls async refuses: the global builtins exist for the value they hand back, so firing one off would leave nothing of it. async is for work that takes time.

Coloque async na frente de uma chamada e ela roda na própria thread virtual — uma thread que o runtime multiplexa sobre threads reais, barata o suficiente para você ter milhares — enquanto quem chamou segue em frente.

proc main(): void {
	async slowShout("logged in the background")
	echo "main did not wait"
}
imprime
main did not wait

Dispare-e-esqueça é tudo o que isto é. Escrito como comando, o resultado da chamada é descartado, e não sobra nada de onde ler um. Dê um nome à mesma chamada e ela guarda o resultado — esse é o passo 91, e o único outro lugar onde async pode ser escrito. Dentro de uma expressão maior não pode: echo len(async f(x)) é erro de compilação, porque ali o valor é desejado na hora, e é justamente isso que uma chamada pura já faz.

Funciona em qualquer chamada — uma func, um proc, uma query, a biblioteca padrão, um serviço (Parte XV). É assim que você inicia um servidor que bloqueia para sempre sem bloquear o programa que o iniciou, o que você vê na Parte XIV.

não compila
async len(v)

O único grupo de chamadas que o async recusa: os builtins globais existem pelo valor que devolvem, então dispará-los não deixaria nada deles. async é para trabalho que leva tempo.

90A mutex crossing threads is copiedUm mutex que cruza threads é copiado

Step 22 gave a proc the caller's own storage to write to. That holds while the caller waits for it. Fire the same call off with async and it does not: a thread of its own gets storage of its own.

proc grow(log: mut Str[dyn], entry: Str): void {
	append(log, entry)
}

proc main(): void {
	mut Str[dyn] log = ["start"]
	grow(log, "waited")        // shares — log becomes [start waited]
	async grow(log, "fired")   // copies — log is untouched
	hive.task.sleep(200)
	echo log
}
prints
[start waited]

The copy is made here, in the caller, before the thread starts — so there is never a moment when both sides could be writing the same vector. A call inside an await list is copied for the same reason, and so is a mutex call nested in one. Keeping the call's result (step 91) changes nothing about this: what decides is the thread, not the name.

Keep in mind

This is the whole of Hive's story for mutable state across threads: not that races are detected, but that nothing reaches another thread without being copied on the way. Two declarations, one shared and one not, are the same declaration — read the call.

O passo 22 deu a uma proc o armazenamento de quem chamou para escrever. Isso vale enquanto quem chamou espera. Dispare a mesma chamada com async e não vale mais: uma thread própria recebe armazenamento próprio.

proc grow(log: mut Str[dyn], entry: Str): void {
	append(log, entry)
}

proc main(): void {
	mut Str[dyn] log = ["start"]
	grow(log, "waited")        // compartilha — log vira [start waited]
	async grow(log, "fired")   // copia — log fica intacto
	hive.task.sleep(200)
	echo log
}
imprime
[start waited]

A cópia é feita aqui, em quem chamou, antes de a thread começar — então nunca existe um instante em que os dois lados poderiam escrever no mesmo vetor. Uma chamada dentro de uma lista await é copiada pelo mesmo motivo, e uma chamada com mutex aninhada nela também. Guardar o resultado da chamada (passo 91) não muda nada disso: quem decide é a thread, não o nome.

Guarde isto

Essa é a história inteira do Hive para estado mutável entre threads: não é que corridas sejam detectadas, é que nada chega a outra thread sem ser copiado no caminho. Duas declarações, uma compartilhada e outra não, são a mesma declaração — leia a chamada.

91Keeping the result, without a handleGuardando o resultado, sem handle

The third shape, and the middle way between the first two. Give the async call a name: it starts exactly as it did a moment ago — its own thread, the caller carries straight on — but this time the result is kept. The wait moves to wherever the name is read, and happens only if the call has not finished by then.

proc main(): void {
	shout := async slowShout("started early")
	echo "main did not wait"
	echo shout            // waits here, for whatever is left of the second
	echo len(shout)       // free: the value is in hand now
}
prints
main did not wait
started early!!!
16

This is the step to read twice, and here is the part to read twice: shout is a Str. Not a Future<Str>, not a task object — there is still no handle, and the language still has no type for one. Nothing to unwrap, nothing to annotate, nothing to .await and so no way to forget to. Every expression in Hive has a type you could write down, and this one's is the type the func already declared. A language with handles has to invent a type you may hold but never annotate, never return and never pass; Hive has nothing of the kind, and now it has the convenience anyway.

Three consequences worth stating outright. Reading the name again is free and cannot answer differently — the value arrives once and stays. A panic inside the call is raised at the first read, in the code that wanted the value. And a name nothing ever reads is fire-and-forget arrived at from the other side: the work still runs, and its result is dropped.

Two things it will not do. mut x := async f() is refused — the name is work in flight, not storage to assign to. And with timeout is refused as well: a bound needs one moment to measure from, and this has as many moments as it has reads (step 95 bounds a wait that is written where it happens).

A terceira forma, e o meio-caminho entre as duas primeiras. Dê um nome à chamada com async: ela começa exatamente como começou há pouco — thread própria, quem chamou segue em frente — mas agora o resultado é guardado. A espera se muda para onde o nome é lido, e só acontece se a chamada não tiver terminado até ali.

proc main(): void {
	shout := async slowShout("started early")
	echo "main did not wait"
	echo shout            // espera aqui, pelo que faltar do segundo
	echo len(shout)       // de graça: o valor já está em mãos
}
imprime
main did not wait
started early!!!
16

Este é o passo para ler duas vezes, e esta é a parte para ler duas vezes: shout é um Str. Não um Future<Str>, não um objeto de tarefa — ainda não existe handle, e a linguagem continua sem ter um tipo para isso. Nada para desembrulhar, nada para anotar, nenhum .await e portanto nenhum jeito de esquecer um. Toda expressão em Hive tem um tipo que você poderia escrever, e o tipo desta é o que a func já declarou. Uma linguagem com handles precisa inventar um tipo que você pode segurar mas nunca anotar, nunca retornar e nunca passar; Hive não tem nada do gênero — e agora tem a conveniência de todo jeito.

Três consequências que merecem ser ditas em voz alta. Ler o nome de novo é de graça e não pode responder diferente — o valor chega uma vez e fica. Um panic dentro da chamada é levantado na primeira leitura, no código que queria o valor. E um nome que ninguém lê é o dispare-e-esqueça pelo outro lado: o trabalho roda, e o resultado dele é descartado.

Duas coisas que ela não faz. mut x := async f() é recusado — o nome é trabalho em andamento, não armazenamento para receber uma atribuição. E with timeout também é recusado: um limite precisa de um instante para contar a partir dele, e este tem tantos instantes quantas leituras tiver (o passo 95 limita uma espera escrita onde ela acontece).

92Many at once: await [ ... ]Vários de uma vez: await [ ... ]

The fourth shape, and the only await there is. List the calls and every one of them starts on its own thread; the whole list is one barrier, resolving in order to a statically-sized vector of their values.

Str[3] shouts = await [slowShout("a"), slowShout("b"), slowShout("c")]
echo shouts[0] + " " + shouts[1] + " " + shouts[2]

Three calls, one second each, about one second in total. And note the type: Str[3] — the length is how many calls you wrote, so shouts[2] needs no guard. Not a dynamic vector, and certainly nothing untyped.

Every entry has to be a call — a value already in hand has nothing to wait for — and they all have to answer with the same type, since one barrier resolves to one vector. A list of one is legal, and still means "on its own thread, then wait".

Read the keyword as "await all". There is deliberately no await for a single call: calling it already waits (step 88), so a keyword there would say nothing.

A quarta forma, e o único await que existe. Liste as chamadas e cada uma delas começa na própria thread; a lista inteira é uma barreira só, que resolve em ordem para um vetor de tamanho estático com os valores delas.

Str[3] shouts = await [slowShout("a"), slowShout("b"), slowShout("c")]
echo shouts[0] + " " + shouts[1] + " " + shouts[2]

Três chamadas, um segundo cada, cerca de um segundo no total. E repare no tipo: Str[3] — o tamanho é quantas chamadas você escreveu, então shouts[2] não precisa de guarda. Não é um vetor dinâmico, e definitivamente não é nada sem tipo.

Cada entrada tem de ser uma chamada — um valor que você já tem não tem o que esperar — e todas têm de responder com o mesmo tipo, já que uma barreira resolve para um vetor. Uma lista de um é válida, e ainda quer dizer "na própria thread, depois espere".

Leia a palavra como "espere todos". Deliberadamente não existe await para uma chamada só: chamá-la já espera (passo 88), então uma palavra ali não diria nada.

93Different types, at the same timeTipos diferentes, ao mesmo tempo

A vector needs one element type, so work of different types cannot be an await-all. This is what step 91 is for — names have no such trouble:

Str message = async slowShout("mixed")
Int count   = async slowCount("mixed")
echo message + " has base length " + hive.conv.its(count)

Two threads, two types, and the echo waits for both — so this costs the slower of the two, not the sum. Take the two async words away and the same two lines are two blocking calls one after the other, costing the sum. One word per line, at the call site, and nothing about either declaration changed.

When the types do match, an await-all is still the better shape: one barrier, one deadline (step 95), and a statically-sized vector to index. Names are for when they do not.

Um vetor precisa de um tipo de elemento, então trabalho de tipos diferentes não pode ser um await-all. É para isso que serve o passo 91 — nomes não têm esse problema:

Str message = async slowShout("mixed")
Int count   = async slowCount("mixed")
echo message + " has base length " + hive.conv.its(count)

Duas threads, dois tipos, e o echo espera pelas duas — então isso custa a mais lenta das duas, não a soma. Tire os dois async e as mesmas duas linhas são duas chamadas bloqueantes, uma depois da outra, custando a soma. Uma palavra por linha, no ponto de chamada, e nada mudou em nenhuma das duas declarações.

Quando os tipos coincidem, o await-all continua sendo a melhor forma: uma barreira, um prazo (passo 95), e um vetor de tamanho estático para indexar. Nomes são para quando não coincidem.

94Waiting for work with no valueEsperando trabalho sem valor

An await-all over void calls has no value to resolve to, so it is a statement: "both of these, then carry on".

await [note("first"), note("second")]
echo "both notes are in"

This is the shape to reach for when you want a batch of side effects to finish before the next line — several files written, several services told — without caring what any of them answered. Each still runs on its own thread, so the batch costs the slowest of them.

Compare the three: fire-and-forget never waits, a named async waits where the name is read, and this waits for all of them, here. Between them you have every combination there is, and each says at the call site which it is.

Um await-all sobre chamadas void não tem valor para resolver, então é um comando: "as duas, e então siga".

await [note("first"), note("second")]
echo "both notes are in"

Esta é a forma para quando você quer que um lote de efeitos termine antes da próxima linha — vários arquivos escritos, vários serviços avisados — sem se importar com o que cada um respondeu. Cada uma ainda roda na própria thread, então o lote custa a mais lenta delas.

Compare as três: dispare-e-esqueça nunca espera, um async com nome espera onde o nome é lido, e este espera por todas, aqui. Entre elas você tem todas as combinações que existem, e cada uma diz, no ponto de chamada, qual é.

95Bounding the wait: with timeoutLimitando a espera: with timeout

An optional clause may follow anything that waits — a call, or a whole await-all — and it changes what that gives you: without it, the value; with it, a Result.

if slowShout("worth waiting for") with timeout 100 is Result.Error(err) {
	echo "gave up after " + hive.conv.its(err.waited) + "ms: " + err.message
}
prints
gave up after 100ms: the task did not finish within 100ms

Running out of patience is a value to handle, not a crash. The error carries waited — the milliseconds you asked for — and a message.

On an await-all the timeout is one deadline across the whole barrier: await [f(a), f(b)] with timeout 500 means "both of them within half a second", and the whole vector fails together.

A void call has no value for the Result to carry, so bounding one is refused rather than given a type with no spelling.

One nicety: timeout is not a reserved word. It means something only in this two-token clause, so it stays available as an ordinary variable name.

Uma cláusula opcional pode seguir qualquer coisa que espera — uma chamada, ou um await-all inteiro — e ela muda o que aquilo devolve: sem ela, o valor; com ela, um Result.

if slowShout("worth waiting for") with timeout 100 is Result.Error(err) {
	echo "gave up after " + hive.conv.its(err.waited) + "ms: " + err.message
}
imprime
gave up after 100ms: the task did not finish within 100ms

Perder a paciência é um valor a tratar, não um crash. O erro carrega waited — os milissegundos que você pediu — e uma message.

Num await-all o timeout é um prazo só para a barreira inteira: await [f(a), f(b)] with timeout 500 quer dizer "os dois dentro de meio segundo", e o vetor inteiro falha junto.

Uma chamada void não tem valor para o Result carregar, então limitar uma delas é recusado em vez de receber um tipo sem grafia.

Um detalhe simpático: timeout não é palavra reservada. Ela significa algo apenas nessa cláusula de dois tokens, então continua disponível como nome de variável.

96A timeout abandons the wait, not the workUm timeout abandona a espera, não o trabalho

This is the step to remember. A virtual thread cannot be stopped from the outside, so a timeout does not cancel anything — it stops you waiting. The call runs on, and only its result was dropped.

if slowShout("worth waiting for") with timeout 100 is Result.Error(err) {
	echo "gave up after " + hive.conv.its(err.waited) + "ms"
}
echo "carrying on while it finishes somewhere else"
prints
gave up after 100ms
carrying on while it finishes somewhere else

So a bounded call is two decisions in one line: start the work, and give up on hearing about it after 100ms. If you want the answer after all, ask again with more patience — that starts fresh work, and the abandoned one finishes unattended.

Este é o passo para guardar. Uma thread virtual não pode ser parada de fora, então um timeout não cancela nada — ele para você de esperar. A chamada continua, e só o resultado dela foi descartado.

if slowShout("worth waiting for") with timeout 100 is Result.Error(err) {
	echo "gave up after " + hive.conv.its(err.waited) + "ms"
}
echo "carrying on while it finishes somewhere else"
imprime
gave up after 100ms
carrying on while it finishes somewhere else

Ou seja, uma chamada limitada são duas decisões numa linha: comece o trabalho, e desista de saber dele depois de 100ms. Se você quiser a resposta afinal, peça de novo com mais paciência — isso começa um trabalho novo, e o abandonado termina sem ninguém olhando.

97The shapes, on a timelineAs formas, numa linha do tempo

All of it in one picture, now that every piece has been introduced. Play each shape and watch where the calling thread waits — and, in two of them, where it does not.

Tudo numa imagem só, agora que cada peça foi apresentada. Rode cada forma e veja onde a thread que chamou espera — e, em duas delas, onde ela não espera.

The top lane is the thread that called. Each lane below it is a virtual thread of its own. Amber is running work; the hatched stretch is the caller blocked waiting for something it started. A faixa de cima é a thread que chamou. Cada faixa abaixo é uma thread virtual própria. Âmbar é trabalho rodando; o trecho hachurado é a chamadora bloqueada esperando por algo que ela começou.

Each shape differs only in where the work runs and whether the caller waits: on the calling thread for a plain call, on a thread of its own and never waited for with async, on a thread of its own and waited for wherever the name is read when the call is given one, on one thread each behind a single barrier for an await-all, and only up to the deadline when a timeout is given.

Cada forma difere apenas em onde o trabalho roda e se a chamadora espera: na própria thread que chamou, numa chamada pura; numa thread própria e sem ninguém esperando, com async; numa thread própria e esperada onde o nome é lido, quando a chamada recebe um nome; numa thread cada atrás de uma barreira só, num await-all; e apenas até o prazo, quando há um timeout.

98hive.task.sleephive.task.sleep

Parks the calling virtual thread for a number of milliseconds. Only that thread — everything else keeps running.

hive.task.sleep(200)     // let a listener bind before dialling it

So two calls that each sleep a second, waited for together in one await, finish in about a second rather than two. A non-positive argument returns immediately.

It is also the honest way to write the small waits a demo needs — letting a server bind before a client connects, or letting a last line land before the program exits.

All of this part, runnable: code-examples/10 - Concurrency/

Estaciona a thread virtual que chamou por um número de milissegundos. Só aquela thread — todo o resto continua rodando.

hive.task.sleep(200)     // deixe um listener subir antes de discar para ele

Então duas chamadas que dormem um segundo cada, esperadas juntas num await, terminam em cerca de um segundo, não dois. Um argumento não-positivo retorna na hora.

É também o jeito honesto de escrever as esperinhas que uma demo precisa — deixar um servidor subir antes de um cliente conectar, ou deixar a última linha sair antes de o programa terminar.

Toda esta parte, rodando: code-examples/10 - Concurrency/
Part XIIParte XII

Reading tables and filesLendo tabelas e arquivos

One keyword covers CSVs, spreadsheets and SQL results — and says in the source which one it is reading.Uma palavra cobre CSVs, planilhas e resultados de SQL — e diz no código qual deles está lendo.

99using: reading a CSVusing: lendo um CSV

using reads a table. Point it at a path and you get a Result<Table, hive.TableError> — a comma-separated CSV, read as rows of cells.

proc main(): void {
	csv := using "./test.csv"
	if csv is Result.Ok(table) {
		echo "{len(table)} rows"
		if len(table) > 1 {
			echo table[1:]        // everything but the header row
		}
	} else if csv is Result.Error(error) {
		echo error.message
	}
}

The result is the Table from step 39 and nothing more exotic, so every vector tool you know already applies to it: len for the row count, row and column for lookups, t[1:] to drop the header.

Notice that the path is relative to the program's working directory, which hive run sets to the folder holding your entrypoint — so using "./test.csv" means the file sitting next to your source, as you would expect.

using lê uma tabela. Aponte para um caminho e você recebe um Result<Table, hive.TableError> — um CSV separado por vírgulas, lido como linhas de células.

proc main(): void {
	csv := using "./test.csv"
	if csv is Result.Ok(table) {
		echo "{len(table)} rows"
		if len(table) > 1 {
			echo table[1:]        // tudo menos a linha de cabeçalho
		}
	} else if csv is Result.Error(error) {
		echo error.message
	}
}

O resultado é o Table do passo 39 e nada mais exótico, então toda ferramenta de vetor que você já conhece se aplica: len para a contagem de linhas, row e column para buscas, t[1:] para descartar o cabeçalho.

Repare que o caminho é relativo ao diretório de trabalho do programa, que o hive run aponta para a pasta do seu arquivo de entrada — então using "./test.csv" significa o arquivo ao lado do seu código, como se espera.

100Saying which reader you wantDizendo qual leitor você quer

Each form of using names its format in the source. That is not decoration — it is what lets the compiler pick the reader and leave the machinery for the others out of your build.

using "./data.csv"                            // comma-separated
using "./data.tsv" as csv separating by "\t"   // another separator
using "./book.xlsx" as xlsx                    // every sheet of a workbook
using "./book.ods" as ods                      // every table of an ODS
using db run allUsers()                        // a declared query, typed rows
using db run raw someSqlText                   // SQL built at runtime

as csv is optional — a bare path is a CSV — and separating by overrides the comma with any string, not just a single character.

formyields
using <path> / as csvResult<Table, hive.TableError>
as xlsx / as odsResult<Table[dyn], hive.TableError>
run <query>whatever the query declared its rows to be
run raw <text>Result<Table, hive.sql.SqlError>

The two spreadsheet readers are written with nothing but the standard library, so a program that opens a workbook still builds with no dependencies and no network.

Cada forma de using nomeia o formato no código. Isso não é enfeite — é o que permite ao compilador escolher o leitor e deixar a maquinaria dos outros fora do seu build.

using "./data.csv"                            // separado por vírgulas
using "./data.tsv" as csv separating by "\t"   // outro separador
using "./book.xlsx" as xlsx                    // toda planilha de uma pasta
using "./book.ods" as ods                      // toda tabela de um ODS
using db run allUsers()                        // uma query declarada, linhas tipadas
using db run raw someSqlText                   // SQL montado em tempo de execução

as csv é opcional — um caminho puro é um CSV — e separating by troca a vírgula por qualquer string, não só um caractere.

formaentrega
using <caminho> / as csvResult<Table, hive.TableError>
as xlsx / as odsResult<Table[dyn], hive.TableError>
run <query>o que a query declarou como suas linhas
run raw <texto>Result<Table, hive.sql.SqlError>

Os dois leitores de planilha são escritos só com a biblioteca padrão, então um programa que abre uma pasta de trabalho continua compilando sem dependências e sem rede.

101A spreadsheet holds many tablesUma planilha guarda várias tabelas

A CSV is one table, so it comes back as one Table. A workbook holds several, so xlsx and ods come back as a Table[dyn] — one Table per sheet, in the order the document keeps them.

Um CSV é uma tabela, então volta como um Table. Uma pasta de trabalho guarda várias, então xlsx e ods voltam como um Table[dyn] — um Table por aba, na ordem em que o documento as guarda.

The real stock.xlsx that ships with the compiler: two sheets, Stock and Sites. Every cell arrives as a Str — a table is Str[dyn][dyn], all the way down. O stock.xlsx real que vem com o compilador: duas abas, Stock e Sites. Cada célula chega como Str — uma tabela é Str[dyn][dyn], do começo ao fim.

Two sheets become sheets[0] and sheets[1]. Sheet 0's rows become vectors of strings: the header [item, restocked, pence] followed by [Beeswax, 2026-07-02, 450] and two more.

Duas abas se tornam sheets[0] e sheets[1]. As linhas da aba 0 se tornam vetores de strings: o cabeçalho [item, restocked, pence] seguido de [Beeswax, 2026-07-02, 450] e outras duas.

proc main(): void {
	book := using "stock.xlsx" as xlsx
	if book is Result.Ok(sheets) {
		echo "the workbook holds {len(sheets)} sheets"
		if len(sheets) > 0 {
			stock := sheets[0]
			echo "items:     " + join(column(stock, "item"), ", ")
			echo "beeswax:   " + join(row(stock, "Beeswax"), " | ")
		}
	} else if book is Result.Error(error) {
		echo "could not read it: {error.message}"
	}
}
prints
the workbook holds 2 sheets
items:     item, Beeswax, Smoker fuel, Hive tool
beeswax:   Beeswax | 2026-07-02 | 450

An empty sheet comes back as an empty Table rather than being skipped, so the positions in the vector still line up with the positions in the document. Rows are padded out to the widest row in their sheet, since a spreadsheet stores no trailing blanks.

proc main(): void {
	book := using "stock.xlsx" as xlsx
	if book is Result.Ok(sheets) {
		echo "the workbook holds {len(sheets)} sheets"
		if len(sheets) > 0 {
			stock := sheets[0]
			echo "items:     " + join(column(stock, "item"), ", ")
			echo "beeswax:   " + join(row(stock, "Beeswax"), " | ")
		}
	} else if book is Result.Error(error) {
		echo "could not read it: {error.message}"
	}
}
imprime
the workbook holds 2 sheets
items:     item, Beeswax, Smoker fuel, Hive tool
beeswax:   Beeswax | 2026-07-02 | 450

Uma aba vazia volta como um Table vazio em vez de ser pulada, então as posições no vetor continuam alinhadas com as posições no documento. As linhas são preenchidas até a largura da maior linha da aba, já que uma planilha não guarda vazios no fim.

102The one thing a workbook lies aboutA única coisa que uma planilha mente

Cells arrive as the file stores them, with one exception worth knowing: xlsx keeps a date as a day count. Left alone, a date column would read as 46205.

So the cell's number format is consulted to catch exactly those, and they are rendered as the date they were displaying:

prints
restocked: restocked, 2026-07-02, 2026-06-18, 2026-07-21

Depending on the format, you get 2026-07-02, 2026-07-02 14:30:00, or 14:30:00 for a time-only cell. Numbers, booleans and cached formula results pass through untouched — only the dates are undone, because only the dates were encoded.

An ods stores real dates, so there is nothing to undo there.

As células chegam como o arquivo as guarda, com uma exceção que vale saber: o xlsx guarda uma data como contagem de dias. Sem intervenção, uma coluna de datas apareceria como 46205.

Então o formato numérico da célula é consultado para pegar exatamente esses casos, e eles são renderizados como a data que estavam exibindo:

imprime
restocked: restocked, 2026-07-02, 2026-06-18, 2026-07-21

Dependendo do formato, você recebe 2026-07-02, 2026-07-02 14:30:00, ou 14:30:00 numa célula só de hora. Números, booleanos e resultados de fórmula em cache passam intactos — só as datas são desfeitas, porque só as datas foram codificadas.

Um ods guarda datas de verdade, então lá não há nada a desfazer.

103hive.file: everything using does not coverhive.file: tudo que o using não cobre

using reads tables. For everything else there is hive.file, and contents move as Str — which holds bytes rather than validated text, so a binary file survives a read-and-write round trip unchanged.

proc main(): void {
	report := "./scratch/report.txt"

	if hive.file.makeDir("./scratch") is Result.Error(error) {
		echo "could not make it: {error.reason}"
		return
	}
	if hive.file.write(report, "stock report\n") is Result.Ok(written) {
		echo "wrote {written} bytes"
	}
	if hive.file.lines(report) is Result.Ok(lines) {
		echo "the file holds {len(lines)} lines"
	}
	if hive.file.read("./scratch/nothing-here.txt") is Result.Error(error) {
		echo "reading {error.path} failed with {error.reason}"
	}
}
prints
wrote 13 bytes
the file holds 1 lines
reading ./scratch/nothing-here.txt failed with NotFound
callwhat it does
read / linesthe whole file / split on newlines
write / appendreplace / add to the end — both report the bytes written
exists / sizea Bool (a directory counts) / bytes
deletea file, or an already-empty directory
list / makeDirentry names, sorted / create with parents
copy / movecopy over the target / rename, which is also how you move

Everything fallible returns a Result whose error carries a short reason"NotFound", "Permission", "Exists" or "Io" — alongside the path and the underlying message. Two details worth knowing: lines drops the empty piece a trailing newline leaves and any Windows carriage returns, and neither write nor append creates missing parent directories — that is what makeDir is for.

Spreadsheets and hive.file, runnable: code-examples/12 - Files and Spreadsheets/

using lê tabelas. Para todo o resto existe o hive.file, e o conteúdo trafega como Str — que guarda bytes em vez de texto validado, então um arquivo binário sobrevive a um ciclo de leitura e escrita sem alteração.

proc main(): void {
	report := "./scratch/report.txt"

	if hive.file.makeDir("./scratch") is Result.Error(error) {
		echo "could not make it: {error.reason}"
		return
	}
	if hive.file.write(report, "stock report\n") is Result.Ok(written) {
		echo "wrote {written} bytes"
	}
	if hive.file.lines(report) is Result.Ok(lines) {
		echo "the file holds {len(lines)} lines"
	}
	if hive.file.read("./scratch/nothing-here.txt") is Result.Error(error) {
		echo "reading {error.path} failed with {error.reason}"
	}
}
imprime
wrote 13 bytes
the file holds 1 lines
reading ./scratch/nothing-here.txt failed with NotFound
chamadao que faz
read / lineso arquivo inteiro / quebrado por linhas
write / appendsubstitui / adiciona ao fim — ambos reportam os bytes escritos
exists / sizeum Bool (diretório conta) / bytes
deleteum arquivo, ou um diretório já vazio
list / makeDirnomes das entradas, ordenados / cria com os pais
copy / movecopia sobre o destino / renomeia, que é também como se move

Tudo que pode falhar retorna um Result cujo erro carrega um reason curto — "NotFound", "Permission", "Exists" ou "Io" — junto com o path e a message de origem. Dois detalhes que valem: lines descarta o pedaço vazio que uma quebra de linha final deixa e também os retornos de carro do Windows, e nem write nem append criam diretórios pai que faltem — para isso existe o makeDir.

Planilhas e hive.file, rodando: code-examples/12 - Files and Spreadsheets/
Part XIIIParte XIII

SQLSQL

A third kind of callable whose body is SQL and whose return type describes its rows.Um terceiro tipo de callable cujo corpo é SQL e cujo tipo de retorno descreve suas linhas.

104query: the third callablequery: o terceiro callable

You have met proc and func. The third is a query: a func whose body is inline SQL.

type User {
	id:   Int
	name: Str
}

query findUser(name: Str): User[dyn] {
	SELECT id, name FROM users WHERE name = {name}
}

The SQL is not a string. It is the body, written where a body goes, and the compiler reads it — which is what lets it check the columns, generate the row mapper, and refuse a query that cannot mean what it says.

Being read, it is also held to SQL's own convention rather than Hive's: the keywords are upper caseSELECT, FROM, WHERE, NOT NULL. Everything else in there is a name the database chose, and keeps whatever spelling it has over there; one that collides with a keyword says so the way SQL always has, in quotes: SELECT "order" FROM ….

You run one with the using … run form:

result := using db run findUser("Ada")

Você já conheceu proc e func. O terceiro é o query: uma func cujo corpo é SQL embutido.

type User {
	id:   Int
	name: Str
}

query findUser(name: Str): User[dyn] {
	SELECT id, name FROM users WHERE name = {name}
}

O SQL não é uma string. Ele é o corpo, escrito onde um corpo vai, e o compilador o lê — é isso que permite conferir as colunas, gerar o mapeador de linhas e recusar uma query que não pode significar o que diz.

Sendo lido, ele também segue a convenção do próprio SQL, não a do Hive: as palavras-chave são maiúsculasSELECT, FROM, WHERE, NOT NULL. Todo o resto ali é um nome que o banco escolheu, e mantém a grafia que tem lá; um que colida com uma palavra-chave se anuncia como o SQL sempre fez, entre aspas: SELECT "order" FROM ….

Você roda uma com a forma using … run:

result := using db run findUser("Ada")

105A query is typed by its rowsUma query é tipada pelas suas linhas

The return type describes what comes back, not the SQL text. Declare User[dyn] and every row is a User.

type User {
	id:     Int
	name:   Str
	active: Bool
}

query allUsers(): User[dyn] {
	SELECT id, name, active FROM users ORDER BY id
}

proc main(): void {
	// ... db opened above ...
	result := using db run allUsers()
	if result is Result.Ok(users) {
		for each user in users {
			echo "{user.id}: {user.name} (active {user.active})"
		}
	} else if result is Result.Error(error) {
		echo "query failed: {error.reason}: {error.message}"
	}
}

So you get user.name — a typed field — rather than a cell looked up by header position. Columns are matched to the row type's fields by name, which means reordering the SELECT cannot silently remap them. A column whose name differs from its field needs an alias: SELECT u.name AS author.

A row type holds scalars only, and each cell is converted to the type its field declared. Booleans have no single spelling across databases — one stores 0 and 1, another answers t and f — so all of them are accepted, and anything else is an error rather than a silent false.

O tipo de retorno descreve o que volta, não o texto SQL. Declare User[dyn] e cada linha é um User.

type User {
	id:     Int
	name:   Str
	active: Bool
}

query allUsers(): User[dyn] {
	SELECT id, name, active FROM users ORDER BY id
}

proc main(): void {
	// ... db aberto acima ...
	result := using db run allUsers()
	if result is Result.Ok(users) {
		for each user in users {
			echo "{user.id}: {user.name} (active {user.active})"
		}
	} else if result is Result.Error(error) {
		echo "query failed: {error.reason}: {error.message}"
	}
}

Ou seja, você recebe user.name — um campo tipado — em vez de uma célula buscada por posição de cabeçalho. As colunas são casadas com os campos do tipo de linha por nome, o que significa que reordenar o SELECT não pode remapeá-las silenciosamente. Uma coluna cujo nome difere do campo precisa de um alias: SELECT u.name AS author.

Um tipo de linha guarda apenas escalares, e cada célula é convertida para o tipo que o campo declarou. Booleanos não têm uma grafia única entre bancos — um guarda 0 e 1, outro responde t e f — então todos são aceitos, e qualquer outra coisa é erro em vez de um false silencioso.

106One column, or no rows at allUma coluna, ou nenhuma linha

Two shorter declarations cover the cases where a row type would be overkill.

query userNames(): Str[dyn] {          // one column needs no row type
	SELECT name FROM users ORDER BY name
}

query deleteUser(id: Int): void {      // a statement reports what it touched
	DELETE FROM users WHERE id = {id}
}
declaredusing conn run q(…) yields
Row[dyn] (a declared type)Result<Row[dyn], hive.sql.SqlError>
Str[dyn], Int[dyn], …that column, as a vector
voidResult<Int, hive.sql.SqlError> — rows affected

Which makes the shape of the result something you can read off the declaration, without looking at the SQL at all:

if using db run insertUser(1, "Ada", true) is Result.Ok(n) {
	echo "inserted {n} row"
}
if using db run userNames() is Result.Ok(names) {
	echo join(names, ", ")
}

Duas declarações mais curtas cobrem os casos em que um tipo de linha seria exagero.

query userNames(): Str[dyn] {          // uma coluna não precisa de tipo de linha
	SELECT name FROM users ORDER BY name
}

query deleteUser(id: Int): void {      // um comando reporta o que ele afetou
	DELETE FROM users WHERE id = {id}
}
declaradousing conn run q(…) entrega
Row[dyn] (um tipo declarado)Result<Row[dyn], hive.sql.SqlError>
Str[dyn], Int[dyn], …aquela coluna, como vetor
voidResult<Int, hive.sql.SqlError> — linhas afetadas

Isso faz do formato do resultado algo que você lê na declaração, sem olhar o SQL:

if using db run insertUser(1, "Ada", true) is Result.Ok(n) {
	echo "inserted {n} row"
}
if using db run userNames() is Result.Ok(names) {
	echo join(names, ", ")
}

107Values are bound, never splicedValores são vinculados, nunca colados

An interpolated {param} in a query body looks like the string interpolation from step 6, but it is doing something quite different. The value never enters the SQL text. It becomes a placeholder, and the value travels beside the text as an argument.

query insertUser(id: Int, name: Str, active: Bool): void {
	INSERT INTO users (id, name, active) VALUES ({id}, {name}, {active})
}

using db run insertUser(2, "O'Brien", true)     // the apostrophe is just data

So nothing a caller supplies can change what a statement means — not an apostrophe, not a semicolon, not a comment marker. Injection is not something you avoid by being careful here; it is structurally unavailable.

The dialect difference is handled for you too: the query is written one way and rewritten for whichever database is on the other end, which is what lets a single declaration serve both.

Um {param} interpolado no corpo de uma query parece a interpolação de string do passo 6, mas faz algo bem diferente. O valor nunca entra no texto SQL. Ele se torna um placeholder, e o valor viaja ao lado do texto como argumento.

query insertUser(id: Int, name: Str, active: Bool): void {
	INSERT INTO users (id, name, active) VALUES ({id}, {name}, {active})
}

using db run insertUser(2, "O'Brien", true)     // o apóstrofo é só dado

Assim, nada que quem chama forneça pode mudar o que um comando significa — nem um apóstrofo, nem um ponto e vírgula, nem um marcador de comentário. Injeção não é algo que você evita tomando cuidado aqui; ela é estruturalmente indisponível.

A diferença de dialeto também é resolvida por você: a query é escrita de um jeito e reescrita para o banco que estiver do outro lado, o que permite que uma única declaração sirva aos dois.

108SELECT * is a compile errorSELECT * é erro de compilação

does not compile
query allUsers(): User[dyn] {
	SELECT * FROM users
}

A star says neither how many columns come back nor what they are called, so there is nothing to match User's fields against — and what it stands for changes the day somebody adds a column to the table.

Spell the columns out, or declare the query as returning a Table and take the rows untyped. Those are the two honest options.

The rule is about the result, so it costs nothing anywhere else. All four of these still compile:

  • count(*) — a call, not a star.
  • a * 2 — multiplication.
  • a star inside a subquery — it belongs to that subquery.
  • a void statement's select list, as in INSERT INTO a SELECT * FROM b — that is not a result at all.
não compila
query allUsers(): User[dyn] {
	SELECT * FROM users
}

Um asterisco não diz quantas colunas voltam nem como se chamam, então não há nada para casar com os campos de User — e o que ele representa muda no dia em que alguém adiciona uma coluna à tabela.

Escreva as colunas, ou declare a query retornando um Table e receba as linhas sem tipo. Essas são as duas opções honestas.

A regra é sobre o resultado, então não custa nada em nenhum outro lugar. Todos estes quatro continuam compilando:

  • count(*) — uma chamada, não um asterisco.
  • a * 2 — multiplicação.
  • um asterisco dentro de uma subquery — ele pertence àquela subquery.
  • a lista de seleção de um comando void, como em INSERT INTO a SELECT * FROM b — isso não é resultado nenhum.

109Optional filters: WHERE { }Filtros opcionais: WHERE { }

Most "dynamic" queries are really a fixed query with optional predicates, and that needs no string building at all. A WHERE block ANDs the predicates whose conditions hold. It is written in upper case, like the clause it becomes; what decides which predicates go into it is Hive's own if, and and or, written like the rest of your program.

query findUsers(name: Str, minId: Int, onlyActive: Bool, onlyFirst: Bool): User[dyn] {
	SELECT id, name, active FROM users
	WHERE {
		if name != ""  { name = {name} }
		if minId > 0   { id >= {minId} }
		or {
			if onlyActive { active = 1 }
			if onlyFirst  { id = 1 }
		}
	}
	ORDER BY id
}

A nested or { } or and { } flips the connective for its own predicates. Three things this gets right without being asked:

  • A group that contributes nothing disappears, rather than leaving a dangling connective.
  • If no predicate is present at all, there is no WHERE clause — so there is no WHERE 1 = 1 to write.
  • A group contributing more than one predicate is parenthesised, so nesting cannot change how the surrounding connective binds.

Every branch's text is fixed at compile time. Only which branches are taken is decided at runtime — which is a much smaller thing to reason about than a query built from strings.

A maior parte das queries "dinâmicas" é, na verdade, uma query fixa com predicados opcionais, e isso não exige montar string nenhuma. Um bloco WHERE junta com AND os predicados cujas condições valem. Ele se escreve em maiúsculas, como a cláusula que vira; quem decide quais predicados entram é o if, o and e o or do próprio Hive, escritos como o resto do seu programa.

query findUsers(name: Str, minId: Int, onlyActive: Bool, onlyFirst: Bool): User[dyn] {
	SELECT id, name, active FROM users
	WHERE {
		if name != ""  { name = {name} }
		if minId > 0   { id >= {minId} }
		or {
			if onlyActive { active = 1 }
			if onlyFirst  { id = 1 }
		}
	}
	ORDER BY id
}

Um or { } ou and { } aninhado inverte o conectivo para os predicados dele. Três coisas que isso acerta sem que você peça:

  • Um grupo que não contribui com nada desaparece, em vez de deixar um conectivo solto.
  • Se nenhum predicado está presente, não existe cláusula WHERE — então não há WHERE 1 = 1 para escrever.
  • Um grupo que contribui com mais de um predicado é parentetizado, então o aninhamento não muda como o conectivo de fora liga.

O texto de cada ramo é fixo em tempo de compilação. Só quais ramos entram é decidido em tempo de execução — o que é bem menos coisa para raciocinar do que uma query montada com strings.

110What a where block deliberately cannot doO que um bloco where deliberadamente não faz

A column name or a sort direction can never be a parameter. It is not an oversight: ORDER BY {col} would sort by a constant string, which is a bug that looks like it works.

So when the ordering genuinely varies, make the choices a variant type and dispatch to one query per ordering. The compiler then checks the match is exhaustive, and injecting a column name is structurally impossible rather than merely discouraged.

type Order {
	ByName
	ByFrames
}

if order is Order.ByName {
	return using db run colonyByName()
} else if order is Order.ByFrames {
	return using db run colonyByFrames()
}

And SQL you genuinely do assemble yourself — an admin console, ad-hoc reporting — goes through run raw, which is the next step.

Um nome de coluna ou uma direção de ordenação nunca pode ser parâmetro. Não é esquecimento: ORDER BY {col} ordenaria por uma string constante, que é um bug com cara de funcionar.

Então, quando a ordenação varia de verdade, transforme as escolhas num tipo com variantes e despache para uma query por ordenação. O compilador então confere que o casamento é exaustivo, e injetar um nome de coluna fica estruturalmente impossível, não apenas desencorajado.

type Order {
	ByName
	ByFrames
}

if order is Order.ByName {
	return using db run colonyByName()
} else if order is Order.ByFrames {
	return using db run colonyByFrames()
}

E o SQL que você realmente monta — um console de administração, relatórios ad-hoc — passa pelo run raw, que é o próximo passo.

111run raw: the escape hatchrun raw: a saída de emergência

SQL assembled at runtime has a shape nothing can know in advance, so it says so — and comes back as a Table with its header row, exactly as reading a CSV does.

if using db run raw "SELECT count(*) AS total FROM users" is Result.Ok(t) {
	echo t
}
prints
[[total] [3]]

It is untyped by construction, and that is the point: every place in a program where SQL is built rather than declared is greppable for exactly these two words. The typed path is the default, and the exception is visible.

SQL montado em tempo de execução tem um formato que nada pode saber de antemão, então ele diz isso — e volta como um Table com a linha de cabeçalho, exatamente como ler um CSV.

if using db run raw "SELECT count(*) AS total FROM users" is Result.Ok(t) {
	echo t
}
imprime
[[total] [3]]

Ele é sem tipo por construção, e essa é a ideia: todo lugar de um programa onde SQL é montado em vez de declarado pode ser encontrado com um grep por essas duas palavras. O caminho tipado é o padrão, e a exceção fica visível.

112Opening a connectionAbrindo uma conexão

hive.sql talks to SQLite and PostgreSQL. The SQLite engine is compiled straight into your executable, so a local database needs nothing installed.

opened := hive.sql.pool(hive.sql.DatabaseDriver.SQLite(), "./demo.db", 4, 2)
if opened is Result.Error(error) {
	echo "could not open database: {error.reason}: {error.message}"
	return
}
if opened is Result.Ok(db) {
	// ... use db ...
	hive.sql.close(db)
}

hive.sql.connect(driver, connString) uses default pool settings; pool(driver, connString, maxOpen, maxIdle) sets them explicitly. The driver is built with DatabaseDriver.SQLite(), .PostgreSQL(), or .Other(name) for anything else registered.

Hold it, don't reopen it

A SqlConnection is a connection pool, not a single connection. It is safe to hold for the life of the program and to share across virtual threads — open it once in main and pass it along, which is what a partial application like handler(_, db) is for. Never open one per query: for a file-backed database that is merely wasteful, and for an in-memory one it is worse, as the next step explains.

One build note: SQL programs link real database drivers, so the first build of a program that uses hive.sql fetches them (network once, then cached). A program that never opens a connection stays dependency-free and builds offline.

O hive.sql fala com SQLite e PostgreSQL. O motor do SQLite é compilado direto dentro do seu executável, então um banco local não precisa de nada instalado.

opened := hive.sql.pool(hive.sql.DatabaseDriver.SQLite(), "./demo.db", 4, 2)
if opened is Result.Error(error) {
	echo "could not open database: {error.reason}: {error.message}"
	return
}
if opened is Result.Ok(db) {
	// ... use db ...
	hive.sql.close(db)
}

hive.sql.connect(driver, connString) usa as configurações padrão de pool; pool(driver, connString, maxOpen, maxIdle) define explicitamente. O driver é montado com DatabaseDriver.SQLite(), .PostgreSQL(), ou .Other(nome) para qualquer outro registrado.

Guarde, não reabra

Um SqlConnection é um pool de conexões, não uma conexão só. É seguro mantê-lo pela vida do programa e compartilhá-lo entre threads virtuais — abra uma vez no main e passe adiante, que é para isso que serve uma aplicação parcial como handler(_, db). Nunca abra um por query: num banco em arquivo isso é só desperdício, e num banco em memória é pior, como o próximo passo explica.

Uma nota de build: programas com SQL ligam drivers de banco reais, então o primeiro build de um programa que usa hive.sql baixa esses drivers (rede uma vez, depois em cache). Um programa que nunca abre conexão continua sem dependências e compila offline.

113The in-memory trapA pegadinha do banco em memória

A plain :memory: database belongs to the connection, not the process. Each connection gets a private, empty database that vanishes when it closes — and combined with pooling, that has a sharp edge.

connection stringone query at a timeeight at once
:memory:, default poolworksfails intermittently: no such table
:memory:, pool(…, 1, 1)worksworks, one at a time
file::memory:?cache=sharedworksworks

The first row is the trap: under concurrency the pool opens further connections, and each one lands on a database of its own. A program that passes every test single-threaded starts failing once requests overlap — which is exactly what happens behind an HTTP server, since it runs each request on its own virtual thread.

opened := hive.sql.pool(hive.sql.DatabaseDriver.SQLite(), "file::memory:?cache=shared", 8, 1)

Ask for a shared cache and every pooled connection sees the same database. Pinning the pool to one connection is also correct, but it serialises every query and gives up the concurrency you had. Two things to know about shared cache: the database lives only while at least one connection is open — which is what that maxIdle of 1 guarantees rather than leaves to the pool's defaults — and it locks at table granularity, so a very write-heavy concurrent workload can meet contention a file-backed database would not.

Um banco :memory: puro pertence à conexão, não ao processo. Cada conexão recebe um banco privado e vazio que desaparece quando ela fecha — e, junto com o pool, isso tem uma ponta afiada.

string de conexãouma query por vezoito ao mesmo tempo
:memory:, pool padrãofuncionafalha às vezes: no such table
:memory:, pool(…, 1, 1)funcionafunciona, uma por vez
file::memory:?cache=sharedfuncionafunciona

A primeira linha é a pegadinha: sob concorrência o pool abre mais conexões, e cada uma cai num banco próprio. Um programa que passa em todo teste em thread única começa a falhar quando os pedidos se sobrepõem — que é exatamente o que acontece por trás de um servidor HTTP, já que ele roda cada pedido na própria thread virtual.

opened := hive.sql.pool(hive.sql.DatabaseDriver.SQLite(), "file::memory:?cache=shared", 8, 1)

Peça um cache compartilhado e toda conexão do pool vê o mesmo banco. Fixar o pool em uma conexão também é correto, mas serializa toda query e abre mão da concorrência que você tinha. Duas coisas sobre o cache compartilhado: o banco vive só enquanto ao menos uma conexão está aberta — que é o que aquele maxIdle de 1 garante, em vez de deixar para o padrão do pool — e ele tranca por tabela, então uma carga concorrente com muita escrita pode encontrar disputa que um banco em arquivo não encontraria.

114What can still failO que ainda pode falhar

A great deal is settled at compile time, but a database is a separate program on the other end of a connection. Four things remain runtime errors, and the reason tells them apart:

reasonwhat happened
"Connection"the connection is not open
"Query"the driver rejected the SQL, or the read failed
"Shape"the row came back with a different number of columns
"Convert"a cell did not fit the type its field was declared with

Which is a short list, and a useful one to branch on. Everything about the query's own correctness — its columns, its parameters, the type of every row — was settled before the program started.

A full SQLite program: code-examples/5 - SQL/

Muita coisa é resolvida em tempo de compilação, mas um banco é outro programa do outro lado de uma conexão. Quatro coisas continuam sendo erros de execução, e o reason as distingue:

reasono que aconteceu
"Connection"a conexão não está aberta
"Query"o driver recusou o SQL, ou a leitura falhou
"Shape"a linha voltou com um número diferente de colunas
"Convert"uma célula não caiu no tipo que o campo declarou

É uma lista curta, e útil para ramificar. Tudo sobre a correção da própria query — as colunas, os parâmetros, o tipo de cada linha — foi resolvido antes de o programa começar.

Um programa SQLite completo: code-examples/5 - SQL/
Part XIVParte XIV

The standard libraryA biblioteca padrão

Everything behind hive. — conversions, JSON, cryptography, networking, the terminal and the clock.Tudo por trás de hive. — conversões, JSON, criptografia, rede, terminal e relógio.

115A module you don't use is not in your buildUm módulo que você não usa não entra no seu build

Each module owns its types under its own namespace — hive.net.HttpRequest, hive.json.JsonError, hive.sql.SqlError. The only types that live directly on hive are the core ones the language uses without a module: Result, Table, and the hive.TableError that using yields.

And a module is written into your build only when your program references it. Not configured, not tree-shaken afterwards — simply not included.

A module that one you use depends on comes along too: reaching for a JWT pulls in hive.crypto, which decodes payloads with hive.json and checks expiry against hive.time, so you get all three.

This is why hive.sql is the only module that costs you anything at build time — it is the only one that links external database drivers. Every other module is written with nothing but the standard library of the language Hive compiles to, so a program that speaks HTTP, WebSockets, TCP, JSON and cryptography still builds offline with no dependencies at all.

Cada módulo tem seus tipos no próprio namespace — hive.net.HttpRequest, hive.json.JsonError, hive.sql.SqlError. Os únicos tipos que ficam direto em hive são os centrais, que a linguagem usa sem módulo: Result, Table, e o hive.TableError que o using entrega.

E um módulo entra no seu build só quando seu programa o referencia. Não é configurado, não é removido depois — simplesmente não é incluído.

Um módulo do qual um módulo que você usa depende vem junto: pedir um JWT traz o hive.crypto, que decodifica payloads com hive.json e confere validade contra o hive.time, então você recebe os três.

É por isso que o hive.sql é o único módulo que custa algo no build — é o único que liga drivers de banco externos. Todo outro módulo é escrito só com a biblioteca padrão da linguagem para a qual o Hive compila, então um programa que fala HTTP, WebSockets, TCP, JSON e criptografia ainda compila offline, sem dependência alguma.

116hive.conv: numbers and texthive.conv: números e texto

The module you will reach for most, because Int and Float do not mix on their own and echo is not always where a number is going.

Int up      = hive.conv.ceil(2.1)      // 3
down       := hive.conv.floor(2.9)     // 2
nearest    := hive.conv.round(2.5)     // 3 — halves round away from zero

Float wide  = hive.conv.itf(nearest)   // Int  -> Float
rendered   := hive.conv.its(up)        // Int  -> Str
also       := hive.conv.fts(wide)      // Float -> Str

// Parsing can fail, so it answers with a Result.
if hive.conv.sti("42") is Result.Ok(n) { echo n }
if hive.conv.stf("nope") is Result.Error(error) {
	echo "could not parse {error.input}: {error.message}"
}
prints
42
could not parse nope: not a valid number

The names are terse on purpose — its is Int-to-Str, stf is Str-to-Float — because they appear inside string building constantly. Everything here is pure, so it works in a func as happily as in a proc.

O módulo que você mais vai usar, porque Int e Float não se misturam sozinhos e o echo não é o único destino de um número.

Int up      = hive.conv.ceil(2.1)      // 3
down       := hive.conv.floor(2.9)     // 2
nearest    := hive.conv.round(2.5)     // 3 — metades arredondam para longe do zero

Float wide  = hive.conv.itf(nearest)   // Int  -> Float
rendered   := hive.conv.its(up)        // Int  -> Str
also       := hive.conv.fts(wide)      // Float -> Str

// Converter texto pode falhar, então responde com um Result.
if hive.conv.sti("42") is Result.Ok(n) { echo n }
if hive.conv.stf("nope") is Result.Error(error) {
	echo "could not parse {error.input}: {error.message}"
}
imprime
42
could not parse nope: not a valid number

Os nomes são curtos de propósito — its é Int para Str, stf é Str para Float — porque aparecem dentro de construção de strings toda hora. Tudo aqui é puro, então funciona numa func tão bem quanto num proc.

117hive.math: the arithmetic the operators cannot spellhive.math: a aritmética que os operadores não escrevem

The operators cover the everyday four, plus % and ** from step 12. Everything trigonometric, radical or bounded is in hive.math — thirteen functions, and one rule for all of them.

what forthe calls
constantspi()
anglessin(x) cos(x) tan(x) asin(x) acos(x) atan2(y, x)
distancessqrt(x) hypot(x, y) abs(x)
boundsmin(a, b) max(a, b) clamp(value, low, high)

Every one of them takes and answers with a Float, and there is no integer twin. That is one rule for thirteen functions, and it is the language's rule rather than the module's: Hive never widens a number behind your back (step 11), so an Int goes in through hive.conv.itf and math.max(0, health) does not build. Clamping an Int is an if, and needs no library.

Everything here is pure, so it works in a func as readily as in a proc — which is what lets a view compute a position, a func decide whether a shot hit, and a test check either one with no machine to run it on.

import hive.math

// Where "forward" is for somebody facing `yaw`: two calls, and no matrix.
func forward(yaw: Float): Float {
	return 0.0 - math.sin(yaw)
}

func distance(ax: Float, az: Float, bx: Float, bz: Float): Float {
	return math.hypot(bx - ax, bz - az)
}

Nothing here reports an error, because none of these have one. A value outside a function's domain answers with the non-finite Float the arithmetic already produces: sqrt(-1.0) and asin(1.5) are NaN, exactly as 10.0 ** 400.0 is +Inf back in step 14.

Two details

atan2 takes the two legs rather than their ratio, which is what lets it tell the four quadrants apart — atan2(y, x), in that order. And clamp is exactly min(max(value, low), high), so bounds handed over the wrong way round answer with high rather than complaining.

Thirteen small functions look like a footnote until Part XVII, where a camera is two numbers read out of the state and "which way am I facing" is a sin and a cos.

Os operadores cobrem os quatro do dia a dia, mais o % e o ** do passo 12. Tudo que é trigonométrico, radical ou limitado está em hive.math — treze funções, e uma regra só para todas elas.

para quêas chamadas
constantespi()
ângulossin(x) cos(x) tan(x) asin(x) acos(x) atan2(y, x)
distânciassqrt(x) hypot(x, y) abs(x)
limitesmin(a, b) max(a, b) clamp(value, low, high)

Todas recebem e devolvem Float, e não existe uma gêmea inteira. É uma regra só para treze funções, e ela é da linguagem, não do módulo: o Hive nunca alarga um número sem você pedir (passo 11), então um Int entra por hive.conv.itf e math.max(0, health) não compila. Limitar um Int é um if, e não precisa de biblioteca.

Tudo aqui é puro, então funciona num func tão bem quanto num proc — o que é o que permite a uma view calcular uma posição, a um func decidir se um tiro acertou, e a um teste conferir qualquer um dos dois sem máquina nenhuma para rodar.

import hive.math

// Onde é "para frente" para quem está virado em `yaw`: duas chamadas, e matriz
// nenhuma.
func forward(yaw: Float): Float {
	return 0.0 - math.sin(yaw)
}

func distance(ax: Float, az: Float, bx: Float, bz: Float): Float {
	return math.hypot(bx - ax, bz - az)
}

Nada aqui reporta erro, porque nenhuma delas tem erro. Um valor fora do domínio da função responde com o Float não finito que a própria aritmética já produz: sqrt(-1.0) e asin(1.5) são NaN, exatamente como 10.0 ** 400.0 é +Inf lá no passo 14.

Dois detalhes

O atan2 recebe os dois catetos, e não a razão entre eles — é isso que o deixa distinguir os quatro quadrantes: atan2(y, x), nessa ordem. E o clamp é exatamente min(max(value, low), high), então limites passados ao contrário respondem high em vez de reclamar.

Treze funçõezinhas parecem nota de pé de página até a Parte XVII, onde a câmera são dois números lidos do estado e "para onde estou virado" é um sin e um cos.

118hive.json: your types are the schemahive.json: seus tipos são o schema

This is step 70, made concrete. hive.json.parse(text) with T derives a decoder for T at compile time.

type Greeting {
	name: Str
}

proc greet(request: hive.net.HttpRequest): hive.net.HttpResponse {
	parsed := hive.json.parse(request.body) with Greeting
	if parsed is Result.Ok(greeting) {
		return reply(200, "Hello, {greeting.name}!")
	} else if parsed is Result.Error(error) {
		return reply(422, "{error.path}: expected {error.expected}, found {error.found}")
	}
}

Missing fields, wrong types and wrong static vector lengths become a Result.Error carrying the exact path that failed — so a decode error tells you where the document disagreed with the type, not merely that it did. Fields the type does not declare are simply ignored.

hive.json.encode(value) derives the encoder from the static type and therefore cannot fail — no Result to unwrap:

echo hive.json.encode(Box(["ada", "grace"], "people"))
prints
{"items":["ada","grace"],"label":"people"}

Variants decode as {"VariantName": {…}}, and a JSON null selects a type's first field-less variant.

Este é o passo 70, na prática. hive.json.parse(texto) with T deriva um decodificador para T em tempo de compilação.

type Greeting {
	name: Str
}

proc greet(request: hive.net.HttpRequest): hive.net.HttpResponse {
	parsed := hive.json.parse(request.body) with Greeting
	if parsed is Result.Ok(greeting) {
		return reply(200, "Hello, {greeting.name}!")
	} else if parsed is Result.Error(error) {
		return reply(422, "{error.path}: expected {error.expected}, found {error.found}")
	}
}

Campos faltando, tipos errados e vetores de tamanho estático errado se tornam um Result.Error carregando o path exato que falhou — então um erro de decodificação diz onde o documento discordou do tipo, e não apenas que discordou. Campos que o tipo não declara são simplesmente ignorados.

hive.json.encode(valor) deriva o codificador do tipo estático e portanto não pode falhar — nenhum Result para desempacotar:

echo hive.json.encode(Box(["ada", "grace"], "people"))
imprime
{"items":["ada","grace"],"label":"people"}

Variantes decodificam como {"NomeDaVariante": {…}}, e um null no JSON seleciona a primeira variante sem campos do tipo.

119JSON you would rather not modelJSON que você prefere não modelar

Sometimes a document is somebody else's and you want one value out of it. Parsing with Table flattens the whole thing into [path, value] rows — the same Table a CSV gives you — and hive.json.get looks a path up in it.

details := hive.json.parse(request.body) with Table
if details is Result.Ok(table) && hive.json.get(table, "details.mood") is Result.Ok(mood) {
	return reply(200, "Hello {greeting.name}, glad you are {mood}!")
}

So the untyped path is still type-safe: you get a Table, not a dynamic value that might be anything. And the encoder re-nests those rows back into real JSON, so a Table field inside a type you do model round-trips properly.

hive.json.table(text) is the third form: it reads a JSON array of flat objects as a headered Table — the same shape using yields from a CSV, which means the same row and column lookups work on it.

Às vezes um documento é de outra pessoa e você quer um valor dele. Fazer parse with Table achata tudo em linhas [caminho, valor] — o mesmo Table que um CSV entrega — e o hive.json.get busca um caminho nele.

details := hive.json.parse(request.body) with Table
if details is Result.Ok(table) && hive.json.get(table, "details.mood") is Result.Ok(mood) {
	return reply(200, "Hello {greeting.name}, glad you are {mood}!")
}

Então o caminho sem tipo continua seguro: você recebe um Table, não um valor dinâmico que pode ser qualquer coisa. E o codificador re-aninha essas linhas de volta em JSON real, então um campo Table dentro de um tipo que você sim modelou faz o ciclo completo direito.

hive.json.table(texto) é a terceira forma: lê um array JSON de objetos planos como um Table com cabeçalho — o mesmo formato que o using entrega de um CSV, o que significa que as mesmas buscas row e column funcionam nele.

120hive.crypto: hashing, encoding, randomhive.crypto: hash, codificação, aleatório

echo hive.crypto.sha256("hello")                       // lowercase hex
echo hive.crypto.sha512("hello")
echo hive.crypto.hmacSha256("message", "shared-key")   // keyed

encoded := hive.crypto.base64Encode("Hive loves bees")
if hive.crypto.base64Decode(encoded) is Result.Ok(text) {
	echo text
}

secret := hive.crypto.randomHex(32)     // 32 random bytes, 64 hex chars

Hashing and encoding cannot fail, so they return plain values; decoding can, so it returns a Result. randomHex is cryptographically random and therefore the right source for a secret or a nonce.

All of it is pure, so all of it works inside a func. A hive.crypto.CryptoError carries a short reason such as "BadSignature", "Expired" or "Malformed".

echo hive.crypto.sha256("hello")                       // hex minúsculo
echo hive.crypto.sha512("hello")
echo hive.crypto.hmacSha256("message", "shared-key")   // com chave

encoded := hive.crypto.base64Encode("Hive loves bees")
if hive.crypto.base64Decode(encoded) is Result.Ok(text) {
	echo text
}

secret := hive.crypto.randomHex(32)     // 32 bytes aleatórios, 64 chars hex

Hash e codificação não podem falhar, então retornam valores simples; decodificar pode, então retorna um Result. O randomHex é criptograficamente aleatório e portanto a fonte certa para um segredo ou um nonce.

Tudo isso é puro, então tudo funciona dentro de uma func. Um hive.crypto.CryptoError carrega um reason curto como "BadSignature", "Expired" ou "Malformed".

121Encrypting under a passwordCifrando sob uma senha

Hashing proves a value was not changed. Encrypting keeps it from being read in the first place, and takes two functions and one password.

sealed := hive.crypto.encrypt("attack at dawn", "correct horse battery staple")
echo sealed        // AVcjVUYO0AqFQvVl8eISTsX2wzYKV5/ZVDTMQ+I2Fx5thI1Zmw...

opened := hive.crypto.decrypt(sealed, "correct horse battery staple")
if opened is Result.Ok(plain) {
	echo plain                                  // attack at dawn
} else if opened is Result.Error(error) {
	echo "not opened ({error.reason})"
}

Sealing cannot fail, so encrypt returns a plain Str — base64, so it goes into a file, a JSON field or a database column as it is. Opening can fail, so decrypt returns a Result.

What is underneath: AES-256-GCM, with the key derived from the password by 600,000 rounds of PBKDF2-HMAC-SHA256 over a random salt. The salt and the nonce are drawn afresh on every call and travel inside the message, so the same text under the same password never comes out the same twice — somebody holding yesterday's copy of your file and today's learns nothing from comparing them.

And it is authenticated. GCM leaves a tag on the ciphertext, so a message edited after it was encrypted is refused rather than opened into something else. A wrong password fails the same way, as "BadSignature": the tag cannot tell you which of the two went wrong, so neither can Hive.

Keep in mind

The 600,000 rounds are the cost an attacker pays per guess, which is what makes a so-so password expensive to break. They are not what makes it a good password. And a decrypted value is an ordinary Str in ordinary memory: encryption protects what is written down, not what is running.

A password manager built on this and on readSecret: code-examples/16 - EXAMPLE APP - Password Vault/

O hash prova que um valor não foi alterado. Cifrar impede que ele seja lido, e são duas funções e uma senha.

sealed := hive.crypto.encrypt("attack at dawn", "correct horse battery staple")
echo sealed        // AVcjVUYO0AqFQvVl8eISTsX2wzYKV5/ZVDTMQ+I2Fx5thI1Zmw...

opened := hive.crypto.decrypt(sealed, "correct horse battery staple")
if opened is Result.Ok(plain) {
	echo plain                                  // attack at dawn
} else if opened is Result.Error(error) {
	echo "not opened ({error.reason})"
}

Selar não pode falhar, então o encrypt devolve um Str simples — em base64, então ele vai para um arquivo, um campo JSON ou uma coluna de banco do jeito que está. Abrir pode falhar, então o decrypt devolve um Result.

O que está por baixo: AES-256-GCM, com a chave derivada da senha por 600.000 rodadas de PBKDF2-HMAC-SHA256 sobre um salt aleatório. O salt e o nonce são sorteados de novo a cada chamada e viajam dentro da mensagem, então o mesmo texto sob a mesma senha nunca sai igual duas vezes — quem tiver a cópia de ontem do seu arquivo e a de hoje não aprende nada comparando as duas.

E é autenticado. O GCM deixa uma tag na cifra, então uma mensagem alterada depois de cifrada é recusada, em vez de aberta em outra coisa. Uma senha errada falha do mesmo jeito, como "BadSignature": a tag não sabe dizer qual dos dois deu errado, então o Hive também não.

Guarde isto

As 600.000 rodadas são o custo que um atacante paga por tentativa, e é isso que torna cara a quebra de uma senha mais ou menos. Não é isso que a torna uma boa senha. E um valor decifrado é um Str comum em memória comum: a cifra protege o que está escrito, não o que está rodando.

Um gerenciador de senhas feito sobre isso e sobre o readSecret: code-examples/16 - EXAMPLE APP - Password Vault/

122JWTs, on the same ideaJWTs, sobre a mesma ideia

A token's claims are just a Hive type. Signing encodes a value of that type as the payload; verifying decodes the payload straight back into it.

type Session {
	user: Str
	role: Str
	exp: Int
}

proc main(): void {
	secret := hive.crypto.randomHex(32)
	token  := hive.crypto.jwtSign(Session("ada", "admin", hive.time.now() + 3600), secret)

	verified := hive.crypto.jwtVerify(token, secret) with Session
	if verified is Result.Ok(session) {
		echo "welcome {session.user} (role: {session.role})"
	} else if verified is Result.Error(error) {
		echo "rejected ({error.reason}): {error.message}"
	}
}
prints
welcome ada (role: admin)

jwtVerify checks the signature and the exp/nbf claims against the current time before decoding. Only HS256 is accepted, so alg: none and algorithm-confusion attacks are rejected outright rather than configured against.

Two more, for narrow purposes. hive.crypto.jwtHeader(token) reads the header (alg/typ/kid) without verifying — for picking a key by kid. And hive.crypto.jwtDecode(token) with T decodes the payload without verifying it at all: for inspection only, never for authorisation.

Hashing, HMAC, base64 and the whole JWT lifecycle: code-examples/4 - Crypto/

As claims de um token são apenas um tipo Hive. Assinar codifica um valor desse tipo como o payload; verificar decodifica o payload de volta direto para ele.

type Session {
	user: Str
	role: Str
	exp: Int
}

proc main(): void {
	secret := hive.crypto.randomHex(32)
	token  := hive.crypto.jwtSign(Session("ada", "admin", hive.time.now() + 3600), secret)

	verified := hive.crypto.jwtVerify(token, secret) with Session
	if verified is Result.Ok(session) {
		echo "welcome {session.user} (role: {session.role})"
	} else if verified is Result.Error(error) {
		echo "rejected ({error.reason}): {error.message}"
	}
}
imprime
welcome ada (role: admin)

O jwtVerify confere a assinatura e as claims exp/nbf contra o horário atual antes de decodificar. Só HS256 é aceito, então alg: none e ataques de confusão de algoritmo são recusados de saída, em vez de configurados contra.

Mais dois, para fins estreitos. hive.crypto.jwtHeader(token) lê o cabeçalho (alg/typ/kid) sem verificar — para escolher uma chave pelo kid. E hive.crypto.jwtDecode(token) with T decodifica o payload sem verificar nada: só para inspeção, nunca para autorização.

Hash, HMAC, base64 e o ciclo completo de JWT: code-examples/4 - Crypto/

123hive.net: an HTTP serverhive.net: um servidor HTTP

A server is one call, and a handler is a callable passed by name — whose declared shape is checked at compile time.

proc main(): void {
	echo "Listening on http://localhost:8080"
	hive.net.httpServe(8080, handle)
}

proc handle(request: hive.net.HttpRequest): hive.net.HttpResponse {
	echo "{request.method} {request.url}"
	headers := [["Content-Type", "text/plain"]]
	return hive.net.HttpResponse(200, headers, "Hello, {request.url}!\n")
}

Requests and responses are built positionally — HttpRequest(method, url, headers, body) and HttpResponse(status, headers, body) — and headers are a Table of [name, value] rows, so step 39's tools work on them.

To perform a request, hive.net.httpRequest(req) gives you Result<HttpResponse, HttpError>, where an Error means no response was obtained at all:

result := hive.net.httpRequest(hive.net.HttpRequest("GET", "https://example.com", [], ""))
if result is Result.Ok(response) {
	return response
} else if result is Result.Error(error) {
	return hive.net.HttpResponse(502, [], "proxy failed: {error.message}\n")
}
Two things to know

httpServe blocks forever, so it usually goes on a virtual thread of its own — started with async (step 89). And it runs your handler once per request, on its own virtual thread, which is what makes step 110's in-memory trap matter.

Um servidor é uma chamada, e um handler é um callable passado por nome — cuja forma declarada é conferida em tempo de compilação.

proc main(): void {
	echo "Listening on http://localhost:8080"
	hive.net.httpServe(8080, handle)
}

proc handle(request: hive.net.HttpRequest): hive.net.HttpResponse {
	echo "{request.method} {request.url}"
	headers := [["Content-Type", "text/plain"]]
	return hive.net.HttpResponse(200, headers, "Hello, {request.url}!\n")
}

Pedidos e respostas são montados por posição — HttpRequest(method, url, headers, body) e HttpResponse(status, headers, body) — e os cabeçalhos são um Table de linhas [nome, valor], então as ferramentas do passo 39 funcionam nelas.

Para fazer um pedido, hive.net.httpRequest(req) devolve Result<HttpResponse, HttpError>, onde um Error quer dizer que não se obteve resposta alguma:

result := hive.net.httpRequest(hive.net.HttpRequest("GET", "https://example.com", [], ""))
if result is Result.Ok(response) {
	return response
} else if result is Result.Error(error) {
	return hive.net.HttpResponse(502, [], "proxy failed: {error.message}\n")
}
Duas coisas para saber

O httpServe bloqueia para sempre, então normalmente vai numa thread virtual própria — iniciado com async (passo 89). E ele roda seu handler uma vez por pedido, em thread virtual própria, que é o que faz a pegadinha do passo 110 importar.

124WebSocketsWebSockets

The same shape at both ends: wsServe accepts connections and wsConnect opens one. The handshake, frame headers, masking, ping/pong and fragmentation are the runtime's business — a program only ever sees whole messages, as Str.

proc echoBack(connection: hive.net.WsConnection): void {
	opening := hive.net.wsRequest(connection)     // the HTTP request that opened it
	echo "server: a client connected to {opening.url}"

	for ;; {
		incoming := hive.net.wsReceive(connection)
		if incoming is Result.Error(error) {
			echo "server: {error.reason} — {error.message}"
			return
		} else if incoming is Result.Ok(message) {
			if message == "bye" {
				hive.net.wsClose(connection)
				return
			}
			hive.net.wsSend(connection, "you said: {message}")
		}
	}
}

func serveForever(): void {
	hive.net.wsServe(9001, echoBack)     // started with `async serveForever()`
}

The handler is a proc(WsConnection): void, run once per connection on its own virtual thread, and the connection closes when it returns. wsSend reports the bytes the message carried; wsRequest hands back the opening HTTP request so a handler can route on its url or authenticate from its headers.

A peer that hangs up is a Result.Error whose reason is "Closed" — the ordinary end of a conversation, not a failure to be surprised by. One virtual thread should own a connection's receiving side.

A mesma forma nas duas pontas: wsServe aceita conexões e wsConnect abre uma. O handshake, os cabeçalhos de frame, o masking, o ping/pong e a fragmentação são assunto do runtime — um programa só vê mensagens inteiras, como Str.

proc echoBack(connection: hive.net.WsConnection): void {
	opening := hive.net.wsRequest(connection)     // o pedido HTTP que abriu a conexão
	echo "server: a client connected to {opening.url}"

	for ;; {
		incoming := hive.net.wsReceive(connection)
		if incoming is Result.Error(error) {
			echo "server: {error.reason} — {error.message}"
			return
		} else if incoming is Result.Ok(message) {
			if message == "bye" {
				hive.net.wsClose(connection)
				return
			}
			hive.net.wsSend(connection, "you said: {message}")
		}
	}
}

func serveForever(): void {
	hive.net.wsServe(9001, echoBack)     // started with `async serveForever()`
}

O handler é um proc(WsConnection): void, rodado uma vez por conexão em thread virtual própria, e a conexão fecha quando ele retorna. O wsSend reporta os bytes que a mensagem levou; o wsRequest devolve o pedido HTTP de abertura para que um handler possa rotear pela url ou autenticar pelos headers.

Um par que desliga é um Result.Error cujo motivo é "Closed" — o fim comum de uma conversa, não uma falha para se assustar. Uma thread virtual deve ser dona do lado receptor de uma conexão.

125Raw TCPTCP puro

Under both of those, plain streams. And a stream is not a queue of messages: socketReceive hands back whatever has arrived so far, so it is the protocol's job to say where a message ends.

proc greetCaller(connection: hive.net.SocketConnection): void {
	peer := hive.net.socketPeer(connection)
	echo "server: {peer} connected"
	hive.net.socketSend(connection, "HELLO. Say QUIT to leave.\n")

	for ;; {
		line := hive.net.socketReceiveLine(connection)
		if line is Result.Error(error) {
			return
		} else if line is Result.Ok(text) {
			if text == "QUIT" {
				hive.net.socketSend(connection, "BYE\n")
				return
			}
			hive.net.socketSend(connection, "ECHO {text}\n")
		}
	}
}

socketReceiveLine blocks for a whole line and trims the trailing newline — the read for line-oriented protocols. socketReceive(connection, bytes) is the raw one: it blocks until at least one byte arrives and returns up to that many, so a short read is normal and code needing an exact count has to keep asking.

socketConnect(host, port) dials, socketPeer is the remote address, and socketClose shuts it down. A SocketError's reason is "Connect", "Closed", "Send" or "Receive".

Two calls in the module name no protocol at all, because they are the network the other three are built on. hive.net.resolve(name) turns a host name into every address behind it — Result<Str[], hive.net.NetError>, in the resolver's own order, since rotating the answers is how a name balances load — and hive.net.localAddress() is the address other machines reach this one on, asked of the routing table rather than guessed from the first interface in the list. Nothing is sent to find out either one. Loopback is deliberately not an answer: a machine holding only 127.0.0.1 has no address a peer could dial, and "NoAddress" is far more use than one that works right up until the other end is on another host.

A server and its own client, in one program each: code-examples/3 - Networking/

Abaixo dos dois, streams puros. E um stream não é uma fila de mensagens: o socketReceive devolve o que chegou até agora, então é trabalho do protocolo dizer onde uma mensagem termina.

proc greetCaller(connection: hive.net.SocketConnection): void {
	peer := hive.net.socketPeer(connection)
	echo "server: {peer} connected"
	hive.net.socketSend(connection, "HELLO. Say QUIT to leave.\n")

	for ;; {
		line := hive.net.socketReceiveLine(connection)
		if line is Result.Error(error) {
			return
		} else if line is Result.Ok(text) {
			if text == "QUIT" {
				hive.net.socketSend(connection, "BYE\n")
				return
			}
			hive.net.socketSend(connection, "ECHO {text}\n")
		}
	}
}

O socketReceiveLine bloqueia por uma linha inteira e corta a quebra final — a leitura para protocolos orientados a linha. O socketReceive(connection, bytes) é o cru: bloqueia até chegar pelo menos um byte e devolve até aquela quantidade, então uma leitura curta é normal e código que precisa de uma contagem exata tem que continuar pedindo.

socketConnect(host, port) disca, socketPeer é o endereço remoto, e socketClose encerra. O motivo de um SocketError é "Connect", "Closed", "Send" ou "Receive".

Duas chamadas do módulo não nomeiam protocolo nenhum, porque são a rede sobre a qual os outros três são construídos. O hive.net.resolve(name) transforma um nome de host em todos os endereços atrás dele — Result<Str[], hive.net.NetError>, na ordem do próprio resolvedor, já que girar as respostas é como um nome equilibra carga — e o hive.net.localAddress() é o endereço pelo qual outras máquinas alcançam esta, perguntado à tabela de rotas em vez de adivinhado pela primeira interface da lista. Nada é enviado para descobrir nenhum dos dois. Loopback deliberadamente não é resposta: uma máquina que só tem 127.0.0.1 não tem endereço que um par possa discar, e "NoAddress" serve muito mais que um que funciona até a outra ponta estar em outra máquina.

Um servidor e o cliente dele, um programa cada: code-examples/3 - Networking/

126hive.env: configurationhive.env: configuração

if hive.env.get("DATABASE_URL") is Result.Ok(url) {
	echo url
} else if hive.env.get("DATABASE_URL") is Result.Error(error) {
	echo "no {error.key}: {error.message}"
}

One function, and it resolves a name in a fixed order: the .env file in the program's own folder; failing that, the .env in the parent folder; failing that, the OS environment.

The .env file is read once, when the first get runs. It is a plain list of NAME=value lines — blank lines and # comments ignored, an optional export prefix allowed, and single or double quotes stripped from a value.

if hive.env.get("DATABASE_URL") is Result.Ok(url) {
	echo url
} else if hive.env.get("DATABASE_URL") is Result.Error(error) {
	echo "no {error.key}: {error.message}"
}

Uma função, e ela resolve um nome numa ordem fixa: o arquivo .env na pasta do próprio programa; se não achar, o .env na pasta pai; se não achar, o ambiente do sistema operacional.

O arquivo .env é lido uma vez, quando o primeiro get roda. É uma lista simples de linhas NOME=valor — linhas vazias e comentários com # ignorados, um prefixo export opcional permitido, e aspas simples ou duplas removidas do valor.

127hive.term: the terminalhive.term: o terminal

echo "This node's own ip:port? (e.g. 127.0.0.1:9100)"
me := hive.term.read()

for each arg in hive.term.args() {
	echo arg
}

read() blocks until the user finishes a line and returns it without the trailing newline. At end of input it returns whatever preceded EOF ("" if nothing).

It parks only the calling virtual thread — so on a thread started with async, the rest of the program keeps running while that thread waits for a human.

args() is the command-line arguments in order, excluding the program name, as a Str[dyn]. print(text) is echo restricted to a Str.

echo "This node's own ip:port? (e.g. 127.0.0.1:9100)"
me := hive.term.read()

for each arg in hive.term.args() {
	echo arg
}

O read() bloqueia até o usuário terminar uma linha e a devolve sem a quebra final. No fim da entrada, devolve o que vier antes do EOF ("" se não vier nada).

Ele estaciona apenas a thread virtual que chamou — então, numa thread iniciada com async, o resto do programa continua rodando enquanto aquela thread espera por um humano.

args() são os argumentos de linha de comando em ordem, sem o nome do programa, como um Str[dyn]. print(texto) é o echo restrito a um Str.

128Reading a secretLendo um segredo

A password typed at a prompt that echoes is a password left on the screen, in the scrollback, and over the shoulder of whoever walks past. readSecret() is read() with the terminal's echo turned off while it waits.

echo "Master password:"
master := hive.term.readSecret()

opened := hive.crypto.decrypt(sealed, master)

Everything else about it is the read you already know: it blocks until Return, hands back the line without its newline, parks only the calling virtual thread, and answers "" at end of input. The only difference is that nothing appears as it is typed — and that when the user presses Return, the cursor moves to the next line, since the echo that would have moved it is off.

The echo goes back on before the call returns. It also goes back on if the program is interrupted at the prompt: a Ctrl-C there ends the program the way an uncaught interrupt always does, but not before putting the terminal back, so the shell that inherits it is never left hiding what is typed into it.

Where there is no terminal to hide anything from — input redirected from a file, a job started without one — the line is read exactly as read() reads it. A program does not have to know which of the two it is running under, and the same call is right in both.

Keep in mind

Hiding the typing is all this does. The line is an ordinary Str once it arrives, and how it is stored afterwards — encrypted, hashed, or written to a log by accident — is still entirely up to the program.

A prompt-driven program built on both reads: code-examples/16 - EXAMPLE APP - Password Vault/

Uma senha digitada num prompt que ecoa é uma senha deixada na tela, no histórico de rolagem e por cima do ombro de quem passar. O readSecret() é o read() com o eco do terminal desligado enquanto ele espera.

echo "Master password:"
master := hive.term.readSecret()

opened := hive.crypto.decrypt(sealed, master)

Todo o resto é a leitura que você já conhece: bloqueia até o Enter, devolve a linha sem a quebra, estaciona apenas a thread virtual que chamou e responde "" no fim da entrada. A única diferença é que nada aparece enquanto é digitado — e que, quando o usuário aperta Enter, o cursor desce para a linha seguinte, já que o eco que o desceria está desligado.

O eco volta antes de a chamada retornar. E volta também se o programa for interrompido no prompt: um Ctrl-C ali encerra o programa como uma interrupção não tratada sempre encerra, mas não antes de devolver o terminal ao que era — então o shell que o herda nunca fica escondendo o que é digitado nele.

Onde não há terminal de quem esconder — entrada redirecionada de um arquivo, um job iniciado sem terminal — a linha é lida exatamente como o read() a lê. O programa não precisa saber sob qual dos dois está rodando, e a mesma chamada está certa nos dois.

Guarde isto

Esconder o que é digitado é tudo o que isso faz. A linha é um Str comum assim que chega, e como ela é guardada depois — cifrada, com hash, ou escrita num log sem querer — continua inteiramente por conta do programa.

Um programa guiado por prompts, feito sobre as duas leituras: code-examples/16 - EXAMPLE APP - Password Vault/

129hive.time: the clockhive.time: o relógio

Times are plain Ints — Unix seconds. There is no date type to learn.

echo hive.time.now()
echo hive.time.format(hive.time.now(), "%Y-%m-%d %H:%M:%S")
echo hive.time.format(1700000000, "%A, %d %B %Y")
echo hive.time.timezone()          // "UTC", "PST", "-03", ...
echo hive.time.timezoneOffset()    // minutes east of UTC: UTC+2 is 120
prints
Tuesday, 14 November 2023

format renders a Unix time in local time with a strftime-style template — familiar rather than clever. Unrecognised %x escapes pass through verbatim.

%Y year (4)%m month%d day%H hour 00–23
%I hour 01–12%M minute%S second%p AM/PM
%A/%a weekday%B/%b month name%Z/%z zone%% a literal %

Horários são Ints simples — segundos Unix. Não há tipo de data para aprender.

echo hive.time.now()
echo hive.time.format(hive.time.now(), "%Y-%m-%d %H:%M:%S")
echo hive.time.format(1700000000, "%A, %d %B %Y")
echo hive.time.timezone()          // "UTC", "PST", "-03", ...
echo hive.time.timezoneOffset()    // minutos a leste de UTC: UTC+2 é 120
imprime
Tuesday, 14 November 2023

O format renderiza um horário Unix em hora local com um gabarito no estilo strftime — familiar em vez de esperto. Escapes %x não reconhecidos passam literalmente.

%Y ano (4)%m mês%d dia%H hora 00–23
%I hora 01–12%M minuto%S segundo%p AM/PM
%A/%a dia da semana%B/%b nome do mês%Z/%z fuso%% um % literal

130hive.map — pairs, reached by keyhive.map — pares, alcançados por chave

A vector is reached by position. When what you have is a name for each value rather than an order, you want the other shape of collection: a map. It is the one collection that is not a vector, so it lives in a module rather than in the syntax.

import hive.map

proc main(): void {
	mut hive.map.Map<Str, Int> counts = hive.map.new()
	hive.map.set(counts, "bee", 1)
	hive.map.set(counts, "hive", 1)
	hive.map.set(counts, "bee", 2)          // replaces; not a second key

	echo len(counts)                        // 2 — `len` counts pairs
	echo hive.map.has(counts, "comb")       // false
}

hive.map.new() is the empty map, and the type is written on the declaration — hive.map.Map<K, T> — because "empty" says nothing about what a map holds, and its key and value types are part of what it is. So m := hive.map.new() is refused, and the error says which line would have said.

Looking one up answers with a Result, because a key that is not there is an ordinary answer rather than a failure — the same reason indexOf answers with one:

if hive.map.get(counts, "bee") is Result.Ok(seen) {
	echo "bee: {seen}"
} else {
	echo "no bees"
}

set and delete change which keys the map has, which is a write — so they need a mut map, exactly as append needs a mut vector. And a map is a value like any other: binding one to a second name copies it whenever the two could otherwise watch each other change (Part V's whole rule applies unchanged).

Um vetor é alcançado por posição. Quando o que você tem é um nome para cada valor, e não uma ordem, você quer a outra forma de coleção: um mapa. É a única coleção que não é um vetor, então ela mora em um módulo em vez de na sintaxe.

import hive.map

proc main(): void {
	mut hive.map.Map<Str, Int> counts = hive.map.new()
	hive.map.set(counts, "bee", 1)
	hive.map.set(counts, "hive", 1)
	hive.map.set(counts, "bee", 2)          // substitui; não é uma segunda chave

	echo len(counts)                        // 2 — `len` conta pares
	echo hive.map.has(counts, "comb")       // false
}

hive.map.new() é o mapa vazio, e o tipo é escrito na declaração — hive.map.Map<K, T> — porque "vazio" não diz nada sobre o que um mapa guarda, e os tipos da chave e do valor fazem parte do que ele é. Então m := hive.map.new() é recusado, e o erro diz qual linha teria dito.

Consultar uma chave responde com um Result, porque uma chave ausente é uma resposta comum, não uma falha — a mesma razão pela qual o indexOf responde com um:

if hive.map.get(counts, "bee") is Result.Ok(seen) {
	echo "bee: {seen}"
} else {
	echo "nenhuma abelha"
}

O set e o delete mudam quais chaves o mapa tem, o que é uma escrita — então eles pedem um mapa mut, exatamente como o append pede um vetor mut. E um mapa é um valor como qualquer outro: vincular um a um segundo nome o copia sempre que os dois poderiam, de outro modo, ver um ao outro mudar (a regra inteira da Parte V vale sem mudanças).

131A map keeps the order you set its keys inUm mapa mantém a ordem em que você definiu as chaves

Reading a map back gives you two vectors, and both are in the order the keys were first set. Not sorted, and not arbitrary:

mut hive.map.Map<Str, Int> ages = hive.map.new()
hive.map.set(ages, "grace", 45)
hive.map.set(ages, "ada", 36)
hive.map.set(ages, "grace", 46)              // a replacement, so the order holds

echo hive.map.keys(ages)                     // [grace ada]
echo hive.map.values(ages)                   // [46 36]
echo ages                                    // {grace: 46, ada: 36}

This is a deliberate choice, and the reason is worth knowing. Go's own map iterates in a randomised order on purpose, so that nothing comes to depend on one. A Hive map built straight on top of that would print something different every run — which would make a program you cannot test. So the order the pairs went in is the order they come back, and two runs always agree.

The order is for reading a map back, not a rank. Two maps holding the same pairs are equal whichever order they were built in, and there is no ordering between maps at all — so sort on a vector of them is a compile error rather than a coin toss.

// Walk it by walking its keys: `for each` over a map is refused, because a turn
// of the loop would have to be handed the key or the value, and it never said which.
for each name in hive.map.keys(ages) {
	if hive.map.get(ages, name) is Result.Ok(age) {
		echo "{name} is {age}"
	}
}

Indexing one is refused for the same kind of reason: m[0] asks for a position, and a map has keys instead. The error points at hive.map.get.

Ler um mapa de volta dá dois vetores, e ambos vêm na ordem em que as chaves foram definidas pela primeira vez. Não ordenada, e não arbitrária:

mut hive.map.Map<Str, Int> ages = hive.map.new()
hive.map.set(ages, "grace", 45)
hive.map.set(ages, "ada", 36)
hive.map.set(ages, "grace", 46)              // uma substituição, então a ordem se mantém

echo hive.map.keys(ages)                     // [grace ada]
echo hive.map.values(ages)                   // [46 36]
echo ages                                    // {grace: 46, ada: 36}

Isso é uma escolha deliberada, e vale saber o motivo. O mapa do próprio Go itera em ordem aleatorizada de propósito, para que nada passe a depender de uma. Um mapa Hive construído direto sobre isso imprimiria algo diferente a cada execução — o que daria um programa impossível de testar. Então a ordem em que os pares entraram é a ordem em que eles voltam, e duas execuções sempre concordam.

A ordem serve para ler o mapa de volta, não é uma classificação. Dois mapas com os mesmos pares são iguais em qualquer ordem em que tenham sido construídos, e não existe ordenação entre mapas — então o sort em um vetor deles é erro de compilação em vez de sorteio.

// Percorra-o percorrendo as chaves: `for each` sobre um mapa é recusado, porque
// uma volta do laço teria de receber a chave ou o valor, e ela nunca disse qual.
for each name in hive.map.keys(ages) {
	if hive.map.get(ages, name) is Result.Ok(age) {
		echo "{name} tem {age}"
	}
}

Indexar um é recusado pelo mesmo tipo de razão: m[0] pede uma posição, e um mapa tem chaves. O erro aponta para o hive.map.get.

132What a key may be, and the Table in betweenO que pode ser chave, e a Table no meio

A key is compared and hashed whole, so it is a Str, Int, Float, Bool or Atom — or a type of your own whose every field is one of those, which is how a composite key works:

type Cell {
	row: Int
	column: Int
}

mut hive.map.Map<Cell, Str> sheet = hive.map.new()
hive.map.set(sheet, Cell(1, 1), "Beeswax")
echo hive.map.has(sheet, Cell(1, 1))      // true — an equal key is the same key

Storage cannot be a key. Two vectors can hold equal contents and still be different storage, so there would be no answer to whether they are the same key — Map<Str[dyn], Int> is a compile error that says so.

A Map<Str, Str> and a Table are two shapes of the same thing, so each reads as the other. That is how a map gets in and out of a CSV, a set of HTTP headers, or anything else that arrives as rows:

Table settings = [["host", "localhost"], ["port", "8080"]]

hive.map.Map<Str, Str> config = hive.map.fromTable(settings)
if hive.map.get(config, "port") is Result.Ok(port) {
	echo "port {port}"
}
echo hive.map.toTable(config)              // [[host localhost] [port 8080]]
What a map does not do

It does not travel and it does not encode. hive.json.encode refuses one, and so does a hive.syslink mailbox: decoding here is by declared shape — every key named, every type known — while a map's keys are whatever was put in it. Send hive.map.toTable(m) and rebuild it with fromTable on the other side.

The whole module, runnable: code-examples/18 - Maps/

Uma chave é comparada e resumida inteira, então ela é um Str, Int, Float, Bool ou Atom — ou um tipo seu cujos campos sejam todos um desses, que é como funciona uma chave composta:

type Cell {
	row: Int
	column: Int
}

mut hive.map.Map<Cell, Str> sheet = hive.map.new()
hive.map.set(sheet, Cell(1, 1), "Beeswax")
echo hive.map.has(sheet, Cell(1, 1))      // true — uma chave igual é a mesma chave

Armazenamento não pode ser chave. Dois vetores podem ter conteúdos iguais e ainda ser armazenamentos diferentes, então não haveria resposta para se são a mesma chave — Map<Str[dyn], Int> é um erro de compilação que diz isso.

Um Map<Str, Str> e uma Table são duas formas da mesma coisa, então cada um se lê como o outro. É assim que um mapa entra e sai de um CSV, de um conjunto de cabeçalhos HTTP, ou de qualquer coisa que chegue como linhas:

Table settings = [["host", "localhost"], ["port", "8080"]]

hive.map.Map<Str, Str> config = hive.map.fromTable(settings)
if hive.map.get(config, "port") is Result.Ok(port) {
	echo "porta {port}"
}
echo hive.map.toTable(config)              // [[host localhost] [port 8080]]
O que um mapa não faz

Ele não viaja e não codifica. O hive.json.encode recusa um, e uma caixa de entrada do hive.syslink também: a decodificação aqui é por forma declarada — toda chave nomeada, todo tipo conhecido — enquanto as chaves de um mapa são o que foi colocado nele. Envie hive.map.toTable(m) e reconstrua com o fromTable do outro lado.

O módulo inteiro, executável: code-examples/18 - Maps/
Part XVParte XV

Services and nodesServiços e nós

Addressable things with a mailbox, reached by the same statement whether they are in this process or on another machine.Coisas endereçáveis com uma caixa de mensagens, alcançadas pelo mesmo comando estejam neste processo ou em outra máquina.

133What a service isO que é um serviço

hive.syslink adds services: long-lived things that own private state only they can touch, have an identity you can pass around, and receive messages in a mailbox.

A service is a different thing from a call you did not wait for, and the contrast is the fastest way to see what a service is for:

async f(x)hive.syslink.Address
lifetimeas long as the call takesunscoped — outlives everything
identitynone, there is nothing to holdyes, that is the point
interactionnone — it keeps nothing backcalled with a message
as a valueit has none at allordinary value; can be sent inside a message
resultnone, unless the call was given a namethe answer, when you wait for it

So Part XI is for work you start and wait for. This part is for things that exist, hold state, and answer questions — a cache, a session store, a queue, a device driver.

O hive.syslink acrescenta serviços: coisas de vida longa que possuem estado privado que só elas tocam, têm uma identidade que você pode passar adiante, e recebem mensagens numa caixa.

Um serviço é uma coisa diferente de uma chamada que você não esperou, e o contraste é o jeito mais rápido de ver para que serve um serviço:

async f(x)hive.syslink.Address
tempo de vidao tempo que a chamada levarsem escopo — sobrevive a tudo
identidadenenhuma, não há o que segurarsim, é justamente o ponto
interaçãonenhuma — não guarda nadachamado com uma mensagem
como valorela não tem valor nenhumvalor comum; pode ir dentro de uma mensagem
resultadonenhum, a não ser que a chamada tenha recebido um nomea resposta, quando você espera por ela

Então a Parte XI é para trabalho que você inicia e espera. Esta parte é para coisas que existem, guardam estado e respondem perguntas — um cache, um repositório de sessões, uma fila, um driver de dispositivo.

134The handler is a fold over the mailboxO handler é um fold sobre a caixa

A service's behaviour is one proc with a fixed shape: state in, one message, the turn's envelope, and the next state out.

proc (State, Message, hive.syslink.Envelope): State
type Op {
	Put { key: Str, value: Str }
	Count
}

proc cache(rows: Table, op: Op, from: hive.syslink.Envelope): Table {
	if op is Op.Put(key, value) {
		mut Table next = rows
		append(next, [key, value])
		return next
	}
	hive.syslink.answer(from, len(rows))
	return rows
}

The compiler enforces that the state going in and the state coming out are the same type. And notice what is not there: no mutex, and no mut on the state. Messages arrive one at a time, in order, and each turn produces the next state — so there is no moment when two things are touching it.

The fold is the mutex. That is the whole payoff of the model, and it is why the distributed cache example can own a database connection with no locking anywhere in it.

O comportamento de um serviço é um proc com forma fixa: estado entra, uma mensagem, o envelope do turno, e o próximo estado sai.

proc (State, Message, hive.syslink.Envelope): State
type Op {
	Put { key: Str, value: Str }
	Count
}

proc cache(rows: Table, op: Op, from: hive.syslink.Envelope): Table {
	if op is Op.Put(key, value) {
		mut Table next = rows
		append(next, [key, value])
		return next
	}
	hive.syslink.answer(from, len(rows))
	return rows
}

O compilador garante que o estado que entra e o estado que sai são do mesmo tipo. E repare no que não está ali: nenhum mutex, e nenhum mut no estado. As mensagens chegam uma por vez, em ordem, e cada turno produz o próximo estado — então não existe momento em que duas coisas estejam mexendo nele.

O fold é o mutex. Esse é o ganho todo do modelo, e é por isso que o exemplo de cache distribuído pode ser dono de uma conexão de banco sem nenhuma trava em lugar algum.

135Starting one, and giving it a nameIniciando um, e dando um nome a ele

box := hive.syslink.spawn(inbox, 0)              // handler by name, initial state
if hive.syslink.register(#Inbox, box) is Result.Error(err) {
	panic err                                     // "Taken" — that name is in use
}

spawn starts the service and returns its address without blocking. The handler is passed by name and its shape is checked at compile time — including through a partial application like cache(_, _, _, db), which is how you give a service something it needs without putting it in the state.

Then two ways to name an address you did not spawn yourself:

mine   := hive.syslink.at(#Inbox)                // this node
theirs := hive.syslink.on(peer, #Inbox)          // the node reachable at `peer`

Both perform no I/O and cannot fail. They are address construction, not a lookup — which is what lets a program name a service that is not running yet, or a node that is temporarily down, and still type-check and run. hive.syslink.stop(address) shuts one down, and calling it twice is harmless.

box := hive.syslink.spawn(inbox, 0)              // handler por nome, estado inicial
if hive.syslink.register(#Inbox, box) is Result.Error(err) {
	panic err                                     // "Taken" — esse nome está em uso
}

O spawn inicia o serviço e retorna o endereço dele sem bloquear. O handler é passado por nome e a forma dele é conferida em tempo de compilação — inclusive através de uma aplicação parcial como cache(_, _, _, db), que é como você dá a um serviço algo de que ele precisa sem colocar isso no estado.

E dois jeitos de nomear um endereço que você não criou:

mine   := hive.syslink.at(#Inbox)                // este nó
theirs := hive.syslink.on(peer, #Inbox)          // o nó alcançável em `peer`

Os dois não fazem I/O e não podem falhar. Eles constroem endereços, não fazem busca — é isso que permite um programa nomear um serviço que ainda não está rodando, ou um nó momentaneamente fora do ar, e ainda assim compilar e rodar. O hive.syslink.stop(endereço) encerra um, e chamá-lo duas vezes é inofensivo.

136You call the addressVocê chama o endereço

There is exactly one way to reach a service: you call its address. There is no send and no call — an address is not a handle you pass to some function, it is the thing you call. And as with a func, the call site decides what it means.

answer := cache(Op.Count())                  // send it, wait for the answer
answer := cache(Op.Count()) with timeout 250 // ...for at most 250ms
async inbox(Note.Say("hi"))                  // send it, wait for nothing
later  := async cache(Op.Count())            // send it now, wait where `later` is read
both   := await [a(m), b(m)]                 // both, one barrier, one deadline

Existe exatamente um jeito de alcançar um serviço: você chama o endereço dele. Não há send nem call — um endereço não é um handle que você passa para alguma função, ele é a coisa que você chama. E, como numa func, o ponto de chamada decide o que aquilo significa.

answer := cache(Op.Count())                  // envia e espera a resposta
answer := cache(Op.Count()) with timeout 250 // ...por no máximo 250ms
async inbox(Note.Say("hi"))                  // envia e não espera nada
later  := async cache(Op.Count())            // envia agora, espera onde `later` for lido
both   := await [a(m), b(m)]                 // os dois, uma barreira, um prazo
Two nodes, identified by where they are. One persistent, encrypted connection per node pair — not per service and not per message — carries everything, replies included. The amber hexagon on each platform is a service and the atom above it is its registered name; the teal block is the main doing the calling. Watch mine(…): the same statement, but it never reaches the wire. Dois nós, identificados por onde estão. Uma conexão persistente e cifrada por par de nós — não por serviço nem por mensagem — carrega tudo, respostas inclusas. O hexágono âmbar em cada plataforma é um serviço e o átomo acima dele é o nome registrado; o bloco azul-esverdeado é o main que faz as chamadas. Repare no mine(…): o mesmo comando, mas ele nunca chega ao fio.

An async send crosses once and nothing comes back. A plain send crosses, waits for the service's turn, and the answer returns on the same connection. A send to a service on this node never crosses at all, and an unanswered request there comes straight back as NoReply. A panic removes the service and leaves the node running.

Um envio com async atravessa uma vez e nada volta. Um envio puro atravessa, espera o turno do serviço, e a resposta volta pela mesma conexão. Um envio a um serviço deste nó não atravessa nada, e um pedido não respondido ali volta na hora como NoReply. Um panic remove o serviço e deixa o nó rodando.

Three properties of the async statement are worth stating outright. It returns void, it never blocks, and it never fails — a dead or unreachable recipient is not an error at the send site. That is precisely what keeps a local send and a remote one the same statement, and failure is discovered through a monitor instead (step 137).

Given a name, the same async is a request again — registered for its answer, and reporting the same failures as a plain send — but the waiting happens wherever the name is read (step 91). That is how one main asks several services at once when their mailbox types differ, and how it goes on drawing a screen while an answer is still crossing the network.

The callee's type is what makes a call a send, never its spelling. A local, a parameter, a vector element and a fresh at(#Name) are all called the same way, and an address that shares a name with a declared func resolves to the address — exactly as a local shadows a declaration everywhere else in the language.

An address carries one message and nothing else. A second argument, a named argument, a missing message and a partial application c(_) are each rejected by name, because each is a mistake about what the value is.

Três propriedades do comando async merecem ser ditas em voz alta. Ele retorna void, nunca bloqueia, e nunca falha — um destinatário morto ou inacessível não é erro no ponto de envio. É exatamente isso que mantém um envio local e um remoto como o mesmo comando, e a falha é descoberta por um monitor (passo 137).

Com um nome, o mesmo async volta a ser um pedido — registrado para a resposta, e relatando as mesmas falhas de um envio puro — mas a espera acontece onde o nome é lido (passo 91). É assim que um main pergunta a vários serviços de uma vez quando os tipos de caixa deles diferem, e como ele segue desenhando uma tela enquanto uma resposta ainda atravessa a rede.

O tipo do chamado é o que faz de uma chamada um envio, nunca a grafia dela. Um local, um parâmetro, um elemento de vetor e um at(#Nome) recém-criado são todos chamados do mesmo jeito, e um endereço que compartilha nome com uma func declarada resolve para o endereço — exatamente como um local sombreia uma declaração em todo o resto da linguagem.

Um endereço carrega uma mensagem e nada mais. Um segundo argumento, um argumento nomeado, uma mensagem faltando e uma aplicação parcial c(_) são recusados nominalmente, porque cada um é um erro sobre o que aquele valor é.

137The reply type is the mailbox typeO tipo da resposta é o tipo da caixa

A service answers with one of its own messages. So a mailbox type is the whole protocol — requests and responses together — and nothing anywhere needs annotating.

type Note {
	Say { text: Str }        // a plain message: nothing is waiting
	HowMany                  // a request...
	Counted { seen: Int }    // ...and the shape of its answer
}
answer := theirs(Note.HowMany()) with timeout 4000
if answer is Result.Ok(reply) && reply is Note.Counted(count) {
	echo "the peer's inbox has seen {count} notes"
} else if answer is Result.Error(err) {
	echo "could not ask the peer: {err.reason} — {err.message}"
}

Two is tests, chained with the && from step 73: the first unwraps the Result, the second narrows the reply to the variant you expected. Nothing new to learn — a reply narrows exactly like any other value.

Waiting for a send is the one place in the module that reports failure, because it is the only one with somewhere to report to. Six reasons arrive here:

"Timeout"you ran out of patience
"Down" / "NoProc"the service died mid-request, or was not there
"Unreachable"the node could not be reached
"Decode"the payload would not decode
"NoReply"the service handled it and never answered

Um serviço responde com uma das suas próprias mensagens. Assim, um tipo de caixa é o protocolo inteiro — pedidos e respostas juntos — e nada em lugar nenhum precisa de anotação.

type Note {
	Say { text: Str }        // uma mensagem simples: nada está esperando
	HowMany                  // um pedido...
	Counted { seen: Int }    // ...e a forma da resposta dele
}
answer := theirs(Note.HowMany()) with timeout 4000
if answer is Result.Ok(reply) && reply is Note.Counted(count) {
	echo "the peer's inbox has seen {count} notes"
} else if answer is Result.Error(err) {
	echo "could not ask the peer: {err.reason} — {err.message}"
}

Dois testes is, ligados pelo && do passo 73: o primeiro desempacota o Result, o segundo estreita a resposta para a variante que você esperava. Nada novo para aprender — uma resposta estreita exatamente como qualquer outro valor.

Esperar por um envio é o único lugar do módulo que reporta falha, porque é o único que tem para quem reportar. Seis motivos chegam aqui:

"Timeout"você perdeu a paciência
"Down" / "NoProc"o serviço morreu no meio do pedido, ou não estava lá
"Unreachable"o nó não pôde ser alcançado
"Decode"o payload não decodificou
"NoReply"o serviço tratou e nunca respondeu

138Forgetting to answer fails fastEsquecer de responder falha rápido

hive.syslink.answer(from, value) replies to whoever is waiting. If the sender used async, nothing is waiting and it is a no-op — so one handler serves both shapes without caring which it was.

Missing a reply on one branch is the easiest mistake to make in service code, and waiting out a timeout is a miserable way to be told: it points at the network when the problem is a missing line. So a request a service handles without answering comes straight back as "NoReply", naming the service — and you write nothing to get that.

// Every envelope here only ever reaches `answer` and `monitor`, so the compiler
// knows none of them can outlive their turn.
} else if note is Note.Ignore {
	echo "  [inbox] ignoring that one on purpose"
	return seen                       // the caller is told at once
}

The compiler decides which services this applies to by asking where the envelope goes. If a handler's envelope only ever reaches answer, self or monitor — the three calls the runtime controls, none of which keep the reply token — it cannot outlive the turn it arrived in. Once that turn ends, no answer can still be coming, so an unanswered request is failed immediately.

O hive.syslink.answer(from, valor) responde a quem estiver esperando. Se quem enviou usou async, ninguém está esperando e isso é um no-op — então um handler serve as duas formas sem se importar com qual era.

Esquecer a resposta em um ramo é o erro mais fácil de cometer em código de serviço, e esperar um timeout inteiro é um jeito péssimo de ser avisado: aponta para a rede quando o problema é uma linha faltando. Então um pedido que um serviço trata sem responder volta na hora como "NoReply", nomeando o serviço — e você não escreve nada para ganhar isso.

// Todo envelope aqui só chega em `answer` e `monitor`, então o compilador sabe
// que nenhum deles sobrevive ao próprio turno.
} else if note is Note.Ignore {
	echo "  [inbox] ignoring that one on purpose"
	return seen                       // quem chamou é avisado na hora
}

O compilador decide a quais serviços isso se aplica perguntando para onde o envelope vai. Se o envelope de um handler só chega em answer, self ou monitor — as três chamadas que o runtime controla, e nenhuma delas guarda o token de resposta — ele não pode sobreviver ao turno em que chegou. Quando aquele turno termina, nenhuma resposta pode estar vindo, então um pedido não respondido falha imediatamente.

139…unless the envelope leaves the turn…a menos que o envelope saia do turno

The other side of the same rule, and the thing it makes possible. If the envelope goes anywhere else — stored in the returned state, handed to a call the handler does not wait for, passed to one of your own procs — a reply may genuinely still be on its way, and the runtime keeps waiting.

// The envelope leaves the turn, so the answer is allowed to arrive later.
proc unhurried(handled: Int, note: Note, from: hive.syslink.Envelope): Int {
	async answerLate(from)            // replies when it is done; nothing waits here
	return handled + 1
}

func answerLate(from: hive.syslink.Envelope): void {
	hive.task.sleep(400)
	hive.syslink.answer(from, Note.Late("worth the wait"))
}

That is the deferred reply: hand the envelope to an async call and the service's turn ends immediately — the mailbox is free for the next message — without the caller being cut off. It is the pattern for work too slow to hold a mailbox open for.

The analysis is per handler and deliberately conservative, in the same spirit as step 46: one escape anywhere switches the fast failure off for that whole service, and anything it cannot vouch for counts as an escape. Being wrong that way costs a timeout; being wrong the other way would cut off a live request.

O outro lado da mesma regra, e o que ela viabiliza. Se o envelope vai para qualquer outro lugar — guardado no estado retornado, entregue a uma chamada que o handler não espera, passado para um proc seu — uma resposta pode realmente estar a caminho, e o runtime continua esperando.

// O envelope sai do turno, então a resposta pode chegar depois.
proc unhurried(handled: Int, note: Note, from: hive.syslink.Envelope): Int {
	async answerLate(from)            // responde quando terminar; nada espera aqui
	return handled + 1
}

func answerLate(from: hive.syslink.Envelope): void {
	hive.task.sleep(400)
	hive.syslink.answer(from, Note.Late("worth the wait"))
}

Essa é a resposta diferida: entregue o envelope a uma chamada com async e o turno do serviço termina na hora — a caixa fica livre para a próxima mensagem — sem cortar quem chamou. É o padrão para trabalho lento demais para manter uma caixa aberta.

A análise é por handler e deliberadamente conservadora, no mesmo espírito do passo 46: um escape em qualquer lugar desliga a falha rápida para aquele serviço inteiro, e tudo que ela não pode garantir conta como escape. Errar desse lado custa um timeout; errar do outro cortaria um pedido vivo.

140Watching another serviceVigiando outro serviço

Since a send never fails, failure has to be discovered somewhere else. That somewhere is a monitor: you ask to be told when a target dies, by delivering a message you chose yourself.

} else if note is Note.Watch(peer) {
	hive.syslink.monitor(from, peer, Note.PeerLost())
	return seen
} else if note is Note.PeerLost {
	echo "  [inbox] !! the peer's inbox is gone"
	return seen
}

Choosing the message yourself is what keeps a mailbox a single user type, with no builtin envelope union mixed into it. Note.PeerLost is an ordinary variant of your own type, and it narrows with is like every other message.

A monitor fires exactly once, and a target on a node that cannot even be reached fires it immediately. hive.syslink.self(from) is the running service's own address — which is how a service hands out a way to reach itself.

Como um envio nunca falha, a falha tem que ser descoberta em outro lugar. Esse lugar é um monitor: você pede para ser avisado quando um alvo morrer, através da entrega de uma mensagem que você mesmo escolheu.

} else if note is Note.Watch(peer) {
	hive.syslink.monitor(from, peer, Note.PeerLost())
	return seen
} else if note is Note.PeerLost {
	echo "  [inbox] !! the peer's inbox is gone"
	return seen
}

Escolher a mensagem você mesmo é o que mantém uma caixa como um único tipo seu, sem nenhuma união de envelope embutida misturada. Note.PeerLost é uma variante comum do seu próprio tipo, e ela estreita com is como toda outra mensagem.

Um monitor dispara exatamente uma vez, e um alvo num nó que não pode nem ser alcançado o dispara imediatamente. O hive.syslink.self(from) é o endereço do próprio serviço em execução — que é como um serviço entrega um jeito de alcançá-lo.

141A crash is local to its serviceUm crash fica local ao serviço

A panic inside a service body kills only that service. Its monitors are told, its callers stop waiting, and the node keeps running.

// The last branch of the handler, reached by Note.Explode.
panic "the inbox was asked to explode"

This is the one place in Hive where panic does not stop the program (step 54), and it is what makes supervision meaningful: "let it crash" is worthless if crashing takes the node down with it.

Which is also why a named address is the only kind that survives its service dying. A named address is re-resolved through the registry on every send, so a replacement registered under the same atom is picked up by every holder without any of them noticing. An anonymous address carries a mailbox id and is dangling the moment its service exits.

That indirection — not lookup — is what names are for.

Um panic dentro do corpo de um serviço mata apenas aquele serviço. Os monitores dele são avisados, quem o chamava para de esperar, e o nó continua rodando.

// O último ramo do handler, alcançado por Note.Explode.
panic "the inbox was asked to explode"

Este é o único lugar do Hive em que o panic não para o programa (passo 54), e é o que torna supervisão algo com sentido: "deixe quebrar" não vale nada se quebrar leva o nó junto.

É também por isso que um endereço nomeado é o único tipo que sobrevive à morte do serviço. Um endereço nomeado é re-resolvido pelo registro em cada envio, então um substituto registrado sob o mesmo átomo é adotado por todos os detentores sem que nenhum perceba. Um endereço anônimo carrega um id de caixa e fica pendurado no instante em que o serviço dele sai.

Essa indireção — não a busca — é para isso que servem os nomes.

142Nodes have no namesNós não têm nome

A service is named by an atom. A node has no name at all — it is identified by where it is.

if hive.syslink.listen(me) is Result.Error(err) {
	panic err
}
echo "listening on {me}, peer at {peer}"

An endpoint is deployment data — an IP, a DNS name, a value from config — so it is a Str, and a peer list is ordinary runtime data that can be computed, read from a file or resolved through DNS. There is no port-mapper daemon and no cluster-wide name registry.

The endpoint you pass to listen is what this node advertises — what it tells peers to dial it on — and the port is taken from it and bound on every interface. Advertising it is what keeps an address this node hands out dialable even after a peer passes it on again. hive.syslink.node() gives you that endpoint back.

hive.syslink.peers() is the other half of that: every node this one is connected to right now, each as the endpoint it advertised, sorted. It keeps no bookkeeping of its own, because a connection is the record — so sending to a node puts it in the list, a node that dials in appears without this node doing anything, and losing the connection takes it out again. Every entry is exactly what on takes, which is what makes it the answer to "who can I call later?".

Node identity would have to be resolved to an endpoint at runtime anyway, and there is nothing for a peer to impersonate when you reached it by dialing it — so making one an atom would have bought nothing and cost the ability to compute a peer list.

Um serviço é nomeado por um átomo. Um não tem nome nenhum — ele é identificado por onde está.

if hive.syslink.listen(me) is Result.Error(err) {
	panic err
}
echo "listening on {me}, peer at {peer}"

Um endpoint é dado de implantação — um IP, um nome DNS, um valor de configuração — então é um Str, e uma lista de pares é dado comum de execução, que pode ser calculado, lido de um arquivo ou resolvido por DNS. Não existe daemon de mapeamento de portas nem registro de nomes do cluster.

O endpoint que você passa ao listen é o que este nó anuncia — o que ele diz aos pares para discarem — e a porta é tirada dele e ligada em todas as interfaces. Anunciar isso é o que mantém discável um endereço que este nó entrega, mesmo depois de um par repassá-lo. O hive.syslink.node() devolve esse endpoint.

O hive.syslink.peers() é a outra metade disso: todos os nós a que este está conectado agora, cada um como o endpoint que anunciou, ordenados. Ele não guarda registro próprio, porque uma conexão é o registro — então mandar mensagem para um nó o coloca na lista, um nó que disca para cá aparece sem este nó fazer nada, e perder a conexão o tira de novo. Cada entrada é exatamente o que o on recebe, e é isso que faz dela a resposta para "para quem eu posso ligar depois?".

Uma identidade de nó teria que ser resolvida para um endpoint em tempo de execução de todo jeito, e não há nada para um par personificar quando você o alcançou discando para ele — então fazer disso um átomo não compraria nada e custaria a capacidade de calcular uma lista de pares.

143On the wireNa rede

You do not have to know any of this to use the module, but knowing it tells you what the module will and will not do for you.

  • One connection per node pair. Not one per service and not one per message — message order is guaranteed between a pair of nodes, and that requires exactly one pipe per pair. Connections are dialed lazily on the first message that needs one, either end may dial, and when both do at once exactly one survives. Replies travel back over the same pipe, so a node that can only dial out still takes part fully.
  • Delivery is best-effort. A health check runs every 15 seconds, and each one that goes unanswered brings the next one forward by 5 — 15s, then 10s, then 5s — so a node that has gone quiet is declared down after 30 seconds rather than the 45 three full periods would spend, failing its outstanding calls and firing its monitors. The check is a round trip (a tick, answered with a pong), and any frame at all counts as an answer. Messages queued when a node is declared down are dropped, and reconnecting does not resurrect them.
  • Messages cross as JSON, using the very same derived codecs from step 114 — so nothing new has to be derived for a type to be sendable. Every frame carries a structural digest of the message type, so a peer built from a different declaration fails loudly instead of decoding another type's bytes.
  • Always encrypted. Every connection is TLS 1.3, mutually authenticated, with no plaintext path and no flag that could negotiate one away — loopback included. Certificates are ephemeral, made at boot and never written to disk: they carry keys, not identity. Identity comes from a cluster secret proven over the pair of certificates as each side locally sees them, which is what stops an attacker who terminates TLS on both ends from relaying one side's proof to the other. The secret never crosses the wire and never encrypts anything, so leaking it tomorrow does not decrypt traffic captured today.

That secret comes from HIVE_SYSLINK_KEY, or from ~/.hive/syslink.key, which is generated with 32 random bytes on first use — so two nodes on one machine are authenticated with no setup at all, and spanning machines means copying one file.

Also worth knowing: a message is copied on its way in, with the same deep copy from Part V, so a recipient can never observe the sender mutating it afterwards. And HIVE_SYSLINK_STRICT=1 forces local sends through the same encode/decode path a remote one takes, so a message that could not survive the wire fails in a single-process run rather than the first time you add a peer.

Not yet built

Rejecting a wrong-typed message at the call site at compile time (the digest catches it at runtime instead); message fragmentation, so a frame over 8 MB is refused rather than split; a WebSocket transport for environments that only forward HTTP; pinned per-node keys as an alternative to the shared secret; and supervision trees.

Two nodes in two terminals: code-examples/13 - Distributed Actors/ · A three-node cache combining services with SQL: code-examples/9 - EXAMPLE APP - Online Cache/

Você não precisa saber nada disso para usar o módulo, mas saber disso conta o que o módulo vai e não vai fazer por você.

  • Uma conexão por par de nós. Não uma por serviço nem uma por mensagem — a ordem das mensagens é garantida entre um par de nós, e isso exige exatamente um tubo por par. As conexões são discadas na primeira mensagem que precisa de uma, qualquer ponta pode discar, e quando as duas discam ao mesmo tempo exatamente uma sobrevive. As respostas voltam pelo mesmo tubo, então um nó que só consegue discar para fora participa plenamente.
  • A entrega é de melhor esforço. Uma verificação de saúde roda a cada 15 segundos, e cada uma que fica sem resposta adianta a próxima em 5 — 15s, depois 10s, depois 5s — então um nó que ficou quieto é declarado fora do ar em 30 segundos em vez dos 45 que três períodos inteiros gastariam, falhando as chamadas pendentes dele e disparando os monitores. A verificação é uma ida e volta (um tick, respondido com um pong), e qualquer frame conta como resposta. Mensagens enfileiradas quando um nó é declarado fora do ar são descartadas, e reconectar não as ressuscita.
  • As mensagens atravessam como JSON, usando os mesmos codecs derivados do passo 114 — então nada novo precisa ser derivado para um tipo ser enviável. Cada frame carrega um digest estrutural do tipo da mensagem, então um par construído a partir de outra declaração falha alto em vez de decodificar os bytes de outro tipo.
  • Sempre cifrado. Toda conexão é TLS 1.3, mutuamente autenticada, sem caminho em texto claro e sem flag que possa negociar isso — loopback incluído. Os certificados são efêmeros, feitos na inicialização e nunca escritos em disco: eles carregam chaves, não identidade. A identidade vem de um segredo de cluster provado sobre o par de certificados como cada lado o vê localmente, que é o que impede um atacante que termina TLS nas duas pontas de repassar a prova de um lado para o outro. O segredo nunca atravessa a rede e nunca cifra nada, então vazá-lo amanhã não decifra o tráfego capturado hoje.

Esse segredo vem de HIVE_SYSLINK_KEY, ou de ~/.hive/syslink.key, que é gerado com 32 bytes aleatórios no primeiro uso — então dois nós numa máquina são autenticados sem nenhuma configuração, e atravessar máquinas significa copiar um arquivo.

Também vale saber: uma mensagem é copiada na entrada, com a mesma cópia profunda da Parte V, então um destinatário nunca observa quem enviou mutando a mensagem depois. E HIVE_SYSLINK_STRICT=1 força envios locais pelo mesmo caminho de codificação e decodificação que um remoto toma, então uma mensagem que não sobreviveria à rede falha numa execução de processo único, e não na primeira vez que você adiciona um par.

Ainda não construído

Recusar uma mensagem de tipo errado no ponto de chamada em tempo de compilação (o digest a pega em tempo de execução); fragmentação de mensagem, então um frame acima de 8 MB é recusado em vez de dividido; um transporte WebSocket para ambientes que só encaminham HTTP; chaves fixadas por nó como alternativa ao segredo compartilhado; e árvores de supervisão.

Dois nós em dois terminais: code-examples/13 - Distributed Actors/ · Um cache de três nós combinando serviços com SQL: code-examples/9 - EXAMPLE APP - Online Cache/
Part XVIParte XVI

Programs in many filesProgramas em vários arquivos

One keyword, a path, and a name to reach it by.Uma palavra, um caminho, e um nome para alcançá-lo.

144importimport

import, written outside any callable, brings another .hive file's declarations into scope.

import ./lib/text
import ./lib/inventory as stock

proc main(): void {
	echo text.repeat("=", 28)
	echo stock.line(stock.Item("Beeswax", 450))
}

The path is relative to the importing file's own directory and leaves the extension off, so ./lib/text is lib/text.hive next door and ../../shared/text climbs out first. It is the file's location that matters, not where you happen to run hive from.

A module is reached through a name: the file's own name by default (./lib/texttext), or whatever as gives it. Use as when two modules would otherwise collide, when a name would clash with something the importing file declares, or when the file name is not usable as a name at all — ./lib/text-utils needs one.

Everything a module declares is visible: procs, funcs, queries and types. There is no pub/priv distinction yet. Modules may import modules of their own, and a file is loaded once however many modules reach for it.

import, escrito fora de qualquer callable, traz as declarações de outro arquivo .hive para o escopo.

import ./lib/text
import ./lib/inventory as stock

proc main(): void {
	echo text.repeat("=", 28)
	echo stock.line(stock.Item("Beeswax", 450))
}

O caminho é relativo ao diretório do próprio arquivo que importa e dispensa a extensão, então ./lib/text é o lib/text.hive ao lado e ../../shared/text sobe primeiro. O que importa é a localização do arquivo, não de onde você roda o hive.

Um módulo é alcançado por um nome: o nome do próprio arquivo por padrão (./lib/texttext), ou o que o as der. Use o as quando dois módulos colidiriam, quando um nome bateria com algo que o arquivo importador declara, ou quando o nome do arquivo não serve como nome — ./lib/text-utils precisa de um.

Tudo que um módulo declara fica visível: procs, funcs, queries e tipos. Ainda não existe distinção pub/priv. Módulos podem importar módulos próprios, e um arquivo é carregado uma vez por mais módulos que o busquem.

145Names carry no baggage across a boundaryNomes não levam bagagem entre módulos

A type or function only ever means what the module you read it in says it means. Two files may each declare their own Align and both stay distinct types.

// This file's Align, and lib/text's Align, are different types.
type Align {
	Centre
}

proc main(): void {
	mine := Align.Centre()                              // ours
	echo text.pad("total", 16, text.Align.Left())       // theirs
}

An imported type is constructed, annotated and matched through the same name its module is reached by — text.Align.Left(), a text.Align parameter, style is text.Align.Left. And an imported callable is an ordinary value, so it is partially applicable and passable like any other:

rule := text.repeat("~", _)
echo rule(28)

This is also what makes step 65's shadowing safe per module: a map declared in one file leaves every other file's bare map alone, because another module's declarations are only ever reached through its alias.

Um tipo ou função só significa o que o módulo em que você o lê diz que significa. Dois arquivos podem declarar cada um o seu Align e os dois continuam tipos distintos.

// O Align deste arquivo, e o Align do lib/text, são tipos diferentes.
type Align {
	Centre
}

proc main(): void {
	mine := Align.Centre()                              // o nosso
	echo text.pad("total", 16, text.Align.Left())       // o deles
}

Um tipo importado é construído, anotado e casado pelo mesmo nome pelo qual o módulo é alcançado — text.Align.Left(), um parâmetro text.Align, style is text.Align.Left. E um callable importado é um valor comum, então é parcialmente aplicável e passável como qualquer outro:

rule := text.repeat("~", _)
echo rule(28)

É isso também que torna o sombreamento do passo 65 seguro por módulo: um map declarado num arquivo deixa o map puro de todo outro arquivo em paz, porque as declarações de outro módulo só são alcançadas pelo alias dele.

146Import cycles are refusedCiclos de import são recusados

Whether direct — a file importing itself — or round any number of steps. And the error prints the loop it found, so you do not have to reconstruct it:

the compiler says
hive: this import forms a cycle:

    lib/inventory.hive
      -> lib/pricing.hive
      -> lib/inventory.hive
A three-file program: code-examples/11 - Modules/

Seja direto — um arquivo importando a si mesmo — ou dando a volta em qualquer número de passos. E o erro imprime o laço que encontrou, para você não ter que reconstruí-lo:

o compilador diz
hive: this import forms a cycle:

    lib/inventory.hive
      -> lib/pricing.hive
      -> lib/inventory.hive
Um programa de três arquivos: code-examples/11 - Modules/

147The same import shortens a library moduleO mesmo import encurta um módulo da biblioteca

A hive.* module can be given a short name the same way a file of your own can. It is worth having anywhere one is reached often — a view tree names its module on every node.

import hive.ui
import hive.net as web

func view(model: Model): ui.View {
	return ui.row([ui.gap(8)], [ui.text([], model.title)])
}

proc handle(request: web.HttpRequest): web.HttpResponse {
	return web.HttpResponse(200, [], "ok")
}

Same feature, same rules: the alias is a name like any other, so a local of that name shadows it, two imports may not share one, and it may not collide with something the file declares.

Three things are its own. The path is the module and nothing else — import hive.ui, never import hive.ui.View, because what a module holds is reached through the name. Without as, the name is the module's own last segment. And nothing is actually imported: the alias is a spelling, so ui.row and hive.ui.row are the same call, both are always available, and neither changes what is linked into the build.

import hive is not a thing to write. The library is reached a module at a time, and the global builtins from step 67 — len, map, sort — were never behind an import at all.

Um módulo hive.* pode ganhar um nome curto do mesmo jeito que um arquivo seu. Vale a pena onde um deles é alcançado com frequência — uma árvore de view nomeia seu módulo em cada nó.

import hive.ui
import hive.net as web

func view(model: Model): ui.View {
	return ui.row([ui.gap(8)], [ui.text([], model.title)])
}

proc handle(request: web.HttpRequest): web.HttpResponse {
	return web.HttpResponse(200, [], "ok")
}

Mesmo recurso, mesmas regras: o alias é um nome como outro qualquer, então um local com esse nome o sombreia, dois imports não podem dividir um, e ele não pode colidir com algo que o arquivo declara.

Três coisas são só dele. O caminho é o módulo e nada mais — import hive.ui, nunca import hive.ui.View, porque o que um módulo guarda é alcançado através do nome. Sem as, o nome é o último segmento do próprio módulo. E nada é de fato importado: o alias é uma grafia, então ui.row e hive.ui.row são a mesma chamada, as duas estão sempre disponíveis, e nenhuma muda o que é linkado no build.

import hive não é algo que se escreva. A biblioteca é alcançada um módulo por vez, e os builtins globais do passo 67 — len, map, sort — nunca estiveram atrás de um import.

148Importing a Go fileImportando um arquivo Go

Hive is Go behind the curtain, so a Go file next to a Hive one can be called from it directly. The path ends in .go, which is compulsory — that is what says the file is Go and not Hive. (A Hive module's .hive is the opposite: never written. Neither spelling should leave you guessing which language you are about to read.)

import ./lib/measures.go

proc main(): void {
	echo measures.grams(measures.Weight(899, "Hive tool"))   // Hive tool: 899 g
	echo measures.tally(["Bee", "hive", "bee"])              // {bee: 2, hive: 1}
}
package measures

type Weight struct {
	Grams int
	Label string
}

func Grams(w Weight) string                { ... }
func Tally(words []string) map[string]int  { ... }

Only what Go exports is reachable, which there means a capitalised name. Hive names callables in camelCase, so the name is reached with its first letter lowered — Grams is measures.grams — while a type keeps its PascalCase, and an exported struct becomes a Hive type of the same shape: measures.Weight is constructed, annotated and matched like a type the program wrote itself.

Every value crossing the boundary is deep-copied, in both directions. That is the whole point of it: a Go function can sort the slice it was handed, keep hold of it, or write through it, and none of that can reach the Hive value it came from.

// `Heaviest` sorts what it is given, in place — the kind of thing Go code does
// and Hive's rules forbid. It is safe because what crossed was a copy.
mut Str[dyn] labels = ["Beeswax", "Smoker fuel", "Hive tool"]
echo measures.heaviest(labels, [450, 1225, 899])   // [Smoker fuel Hive tool Beeswax]
echo labels                                        // [Beeswax Smoker fuel Hive tool]

It is also why an imported Go function is a func rather than a proc: what a func promises in Hive is that it cannot write to storage its caller can see (step 68), and nothing on the far side of a copy can.

The signatures are read by the Go toolchain rather than guessed at — a small program using Go's own parser, which the compiler writes into its cache the first time and compiles once. So hive check on a program that imports Go needs go on the PATH, as a build already did. The boundary is narrow, and everything outside it is a compile error naming the parameter it could not take:

string          <-> Str            []T      <-> T[dyn]
int             <-> Int            [][]string <-> Table
float64         <-> Float          map[K]V  <-> hive.map.Map<K, T>
bool            <-> Bool           a struct the file exports <-> a Hive type
(T, error)       -> Result<T, Str>  error    ->  Result<Bool, Str>

A pointer, a channel, an interface, a function value, a variadic parameter, an int64 (Hive's Int is Go's int, not a conversion away from one), an unexported field: each is refused, with a message saying why. Go's two-value form is the one that maps onto something familiar — (T, error) is what a Result<T, Str> already means:

if measures.parse("450 heavy") is Result.Error(why) {
	echo "could not weigh it: {why}"
}

A Go map arriving in Hive has its keys sorted, because a Go map has no order of its own to hand over (step 131). And the imported file may import whatever it likes: a build that sees a third-party import resolves it first, so that build needs the network once.

O Hive é Go por baixo do capô, então um arquivo Go ao lado de um Hive pode ser chamado direto por ele. O caminho termina em .go, e isso é obrigatório — é o que diz que o arquivo é Go e não Hive. (O .hive de um módulo Hive é o oposto: nunca escrito. Nenhuma das duas grafias deve deixar você adivinhando qual linguagem está a ponto de ler.)

import ./lib/measures.go

proc main(): void {
	echo measures.grams(measures.Weight(899, "Hive tool"))   // Hive tool: 899 g
	echo measures.tally(["Bee", "hive", "bee"])              // {bee: 2, hive: 1}
}
package measures

type Weight struct {
	Grams int
	Label string
}

func Grams(w Weight) string                { ... }
func Tally(words []string) map[string]int  { ... }

Só o que o Go exporta é alcançável, o que lá quer dizer nome com inicial maiúscula. O Hive nomeia callables em camelCase, então o nome é alcançado com a primeira letra minúscula — Grams é measures.grams — enquanto um tipo mantém o PascalCase, e um struct exportado se torna um tipo Hive da mesma forma: measures.Weight é construído, anotado e casado como um tipo que o próprio programa escreveu.

Todo valor que atravessa a fronteira é copiado em profundidade, nas duas direções. É esse o sentido dela: uma função Go pode ordenar o slice que recebeu, guardá-lo, ou escrever por meio dele, e nada disso alcança o valor Hive de onde ele veio.

// `Heaviest` ordena o que recebe, no lugar — o tipo de coisa que código Go faz
// e as regras do Hive proíbem. É seguro porque o que atravessou foi uma cópia.
mut Str[dyn] labels = ["Beeswax", "Smoker fuel", "Hive tool"]
echo measures.heaviest(labels, [450, 1225, 899])   // [Smoker fuel Hive tool Beeswax]
echo labels                                        // [Beeswax Smoker fuel Hive tool]

É também por isso que uma função Go importada é um func e não um proc: o que um func promete no Hive é que ele não pode escrever em armazenamento que seu chamador enxerga (passo 68), e nada do outro lado de uma cópia pode.

As assinaturas são lidas pelo toolchain do Go, não adivinhadas — um programa pequeno usando o parser do próprio Go, que o compilador escreve no seu cache na primeira vez e compila uma vez. Então hive check em um programa que importa Go precisa do go no PATH, como um build já precisava. A fronteira é estreita, e tudo fora dela é erro de compilação nomeando o parâmetro que ele não pôde receber:

string          <-> Str            []T      <-> T[dyn]
int             <-> Int            [][]string <-> Table
float64         <-> Float          map[K]V  <-> hive.map.Map<K, T>
bool            <-> Bool           um struct exportado pelo arquivo <-> um tipo Hive
(T, error)       -> Result<T, Str>  error    ->  Result<Bool, Str>

Um ponteiro, um canal, uma interface, um valor de função, um parâmetro variádico, um int64 (o Int do Hive é o int do Go, não uma conversão de distância), um campo não exportado: cada um é recusado, com uma mensagem dizendo por quê. A forma de dois valores do Go é a que mapeia em algo familiar — (T, error) é o que um Result<T, Str> já significa:

if measures.parse("450 heavy") is Result.Error(why) {
	echo "não deu para pesar: {why}"
}

Um mapa Go que chega ao Hive vem com as chaves ordenadas, porque um mapa Go não tem ordem própria para entregar (passo 131). E o arquivo importado pode importar o que quiser: um build que vê uma dependência de terceiros a resolve primeiro, então esse build precisa da rede uma vez.

149Importing from a repositoryImportando de um repositório

An import may name a git repository and a file inside it. The path is a host, an owner and a repository — then the path within it, which leaves .hive off exactly as a local import does:

import https://github.com/owner/repo/src/text               // a Hive module
import https://github.com/owner/repo@a1b2c3/src/text        // pinned to a commit
import https://github.com/owner/repo/go/util.go as helpers  // a Go file, same rules
import "https://github.com/owner/repo/lib code/text"        // quoted: the path has a space

The module is named after the file inside the repository, not after the repository or its host (.../src/texttext) — so moving a module out to a repository does not change how the code using it reads. A revision goes where the repository ends: a commit, a tag or a branch.

Any path may be quoted, and one holding a space has to be. Inside the quotes everything up to the closing one is the path, so nothing needs escaping or encoding — and as after it still reads as as.

Everything is fetched once. The repository is cloned into ~/.hive/pkg/<host>_<owner>_<repo>@<commit>, shared by every program on the machine that wants that commit, and nothing touches the network when the clone is already there. An import that named no revision is pinned on first use: the commit it resolved to is written beside the entrypoint, in main.hive's case as main.hive-lock.

# Written by the Hive compiler: the commit each remote import was
# resolved to. Keep it in version control — it is what makes another
# machine build the same program. Delete a line to take the latest.
https://github.com/R0DR160HM/hive-lang 3b4ae8e176fc35fb3d74076156e444e835522714

So the second build needs no network and gets the same code as the first, and a build from the same lock file gets the same code on anybody's machine. Keep the file in version control; delete a line from it to take the latest of that repository again.

A remote module is an ordinary module: it may import its own siblings (resolved inside the clone), import further repositories, and it is loaded once however many modules reach for it. It is also a different module from the same file read locally — two copies of one file are two modules, with types of their own, exactly as step 145 says.

What can go wrong, and what it says

Fetching needs git on the PATH. A missing one, an unreachable host, a revision that does not exist, and a path the repository does not have are four different errors, and each says which it is.

All three kinds of import in one program: code-examples/11 - Modules/

Um import pode nomear um repositório git e um arquivo dentro dele. O caminho é um host, um dono e um repositório — depois o caminho dentro dele, que dispensa o .hive exatamente como um import local:

import https://github.com/owner/repo/src/text               // um módulo Hive
import https://github.com/owner/repo@a1b2c3/src/text        // fixado num commit
import https://github.com/owner/repo/go/util.go as helpers  // um arquivo Go, mesmas regras
import "https://github.com/owner/repo/lib code/text"        // com aspas: o caminho tem espaço

O módulo recebe o nome do arquivo dentro do repositório, não do repositório nem do host (.../src/texttext) — então mover um módulo para um repositório não muda como o código que o usa se lê. Uma revisão vai onde o repositório termina: um commit, uma tag ou um branch.

Qualquer caminho pode vir entre aspas, e um que tenha espaço precisa vir. Dentro das aspas tudo até a de fechamento é o caminho, então nada precisa de escape ou codificação — e o as depois dele continua sendo as.

Tudo é buscado uma vez. O repositório é clonado em ~/.hive/pkg/<host>_<dono>_<repo>@<commit>, compartilhado por todo programa da máquina que queira aquele commit, e nada toca a rede quando o clone já está lá. Um import que não nomeou revisão é fixado no primeiro uso: o commit para o qual ele resolveu é escrito ao lado do entrypoint — no caso de main.hive, em main.hive-lock.

# Written by the Hive compiler: the commit each remote import was
# resolved to. Keep it in version control — it is what makes another
# machine build the same program. Delete a line to take the latest.
https://github.com/R0DR160HM/hive-lang 3b4ae8e176fc35fb3d74076156e444e835522714

Então o segundo build não precisa de rede e recebe o mesmo código do primeiro, e um build a partir do mesmo lock file recebe o mesmo código na máquina de qualquer pessoa. Mantenha o arquivo no controle de versão; apague uma linha dele para voltar a pegar o mais recente daquele repositório.

Um módulo remoto é um módulo comum: ele pode importar os próprios vizinhos (resolvidos dentro do clone), importar outros repositórios, e é carregado uma vez por mais módulos que o busquem. Ele também é um módulo diferente do mesmo arquivo lido localmente — duas cópias de um arquivo são dois módulos, com tipos próprios, exatamente como diz o passo 145.

O que pode dar errado, e o que ele diz

Buscar precisa do git no PATH. Um git ausente, um host inalcançável, uma revisão que não existe, e um caminho que o repositório não tem são quatro erros diferentes, e cada um diz qual é.

Os três tipos de import em um programa: code-examples/11 - Modules/
Part XVIIParte XVII

Graphic InterfacesInterfaces Gráficas

A view is a value, and who paints it is somebody else's business.Uma view é um valor, e quem a desenha é problema de outro.

150A view is a valueUma view é um valor

hive.ui draws things, and the first thing to know is what it does not hand you: there is no window object, no widget to hold on to, nothing with a setText. A view is a tree of values built by ordinary calls, exactly like a vector or a struct is.

import hive.ui

func view(model: Model): ui.View {
	return ui.column([ui.pad(24), ui.gap(12)], [
		ui.text([ui.size(ui.TextSize.Title())], "Orders"),
		ui.text([ui.tone(ui.Tone.Muted())], "{len(model.rows)} of them"),
		ui.table([], model.rows)
	])
}

Because it is a value, composing views is just calling functions — statCard below is a func and nothing more, usable anywhere a view goes:

func statCard(label: Str, value: Str): ui.View {
	return ui.column([ui.pad(16), ui.gap(2)], [
		ui.text([ui.size(ui.TextSize.Title())], value),
		ui.text([ui.size(ui.TextSize.Caption()), ui.tone(ui.Tone.Muted())], label)
	])
}

And the view is a func, which is not a formality. A func cannot call a proc and cannot hold a mutex, so drawing cannot act — which matters, because a repaint happens every time the state changes, and anything a view did would happen again with it. Hand window a proc and it says so.

hive.ui desenha coisas, e a primeira coisa a saber é o que ele não te dá: não existe objeto de janela, nem widget para segurar, nada com um setText. Uma view é uma árvore de valores construída por chamadas comuns, exatamente como um vetor ou uma struct.

import hive.ui

func view(model: Model): ui.View {
	return ui.column([ui.pad(24), ui.gap(12)], [
		ui.text([ui.size(ui.TextSize.Title())], "Pedidos"),
		ui.text([ui.tone(ui.Tone.Muted())], "{len(model.rows)} deles"),
		ui.table([], model.rows)
	])
}

Por ser um valor, compor views é só chamar funções — o statCard abaixo é um func e nada mais, usável em qualquer lugar onde cabe uma view:

func statCard(label: Str, value: Str): ui.View {
	return ui.column([ui.pad(16), ui.gap(2)], [
		ui.text([ui.size(ui.TextSize.Title())], value),
		ui.text([ui.size(ui.TextSize.Caption()), ui.tone(ui.Tone.Muted())], label)
	])
}

E a view é um func, o que não é formalidade. Um func não pode chamar um proc nem segurar um mutex, então desenhar não pode agir — o que importa, porque um redesenho acontece toda vez que o estado muda, e o que uma view fizesse aconteceria de novo junto. Passe um proc para o window e ele avisa.

151Attributes, then childrenAtributos, depois filhos

Every widget takes the same two things: a vector of attributes, then whatever its kind carries. Seventeen widgets, one shape.

ui.row(attrs, children)      // a container: its children
ui.text(attrs, content)      // a leaf: its value
ui.button(attrs, label)
ui.table(attrs, rows)        // the headered Table everything else hands back

Hive has no optional parameters, so what would be an optional argument in another language is an entry in that vector — and an empty one is an ordinary value rather than a special case:

ui.text([], post.body)                       // nothing to say about it
ui.text([ui.tone(ui.Tone.Danger())], "!")    // something to say about it

Attributes are semantic tokens rather than CSS values. ui.tone(ui.Tone.Danger()) can be painted red by a browser, or by a terminal, or by something with no colours at all: it names a role, and what a role looks like is the renderer's business.

Two variants break that rule on purpose, because some colours are data — there is no role called "Ada", and no closed set could have one. Tone.HEX and Tone.RGBA carry a colour outright, and background takes the very same Tone that text does:

ui.text([ui.background(ui.Tone.Danger())], "!")                 // a role
ui.text([ui.background(ui.Tone.RGBA(29, 155, 240, 255))], "!")  // a colour

A role becomes a class the stylesheet answers for; a colour becomes a declaration. Which of the two you wrote is what decides, and the renderer makes that call — a program cannot ask for a class. That is the whole cost, and it is worth naming: a view built only from roles draws anywhere, and those two calls are what spends it. Reach for them when the colour comes from the value — one per user, per category, per series — and for a role when it means good, risky or quiet, because a role is the only one of the two a theme can restyle. A colour is checked rather than trusted on the way through: a hex that is not a hex is Normal, not text smuggled into a stylesheet.

Which is also why the enumerations are types rather than atoms. An atom would work, and would read better — but the atom table belongs to your program, numbered in order of first mention, and a library adding to it would renumber the atoms you wrote. A closed set of variants costs a few characters and takes nothing away from you.

All seventeen of them are in the next step, along with the rule that decided the number: a widget is there only if it cannot be composed from the others. A card is a padded column; a toast is an overlay and a message. link is the odd one out, and it earns its place by being the only one that works with no event at all.

Todo widget recebe as mesmas duas coisas: um vetor de atributos, e depois o que o seu tipo carrega. Dezessete widgets, um formato só.

ui.row(attrs, children)      // um container: seus filhos
ui.text(attrs, content)      // uma folha: seu valor
ui.button(attrs, label)
ui.table(attrs, rows)        // a Table com cabeçalho que todo o resto devolve

Hive não tem parâmetros opcionais, então o que seria um argumento opcional em outra linguagem é uma entrada nesse vetor — e um vetor vazio é um valor comum, não um caso especial:

ui.text([], post.body)                       // nada a dizer sobre ele
ui.text([ui.tone(ui.Tone.Danger())], "!")    // algo a dizer sobre ele

Atributos são tokens semânticos, e não valores de CSS. ui.tone(ui.Tone.Danger()) pode ser pintado de vermelho por um navegador, por um terminal, ou por algo sem cor nenhuma: ele nomeia um papel, e a aparência de um papel é assunto do renderizador.

Duas variantes quebram essa regra de propósito, porque algumas cores são dados — não existe papel chamado "Ada", e nenhum conjunto fechado poderia ter um. Tone.HEX e Tone.RGBA carregam uma cor diretamente, e background recebe exatamente o mesmo Tone que o texto recebe:

ui.text([ui.background(ui.Tone.Danger())], "!")                 // um papel
ui.text([ui.background(ui.Tone.RGBA(29, 155, 240, 255))], "!")  // uma cor

Um papel vira uma classe que a folha de estilo responde; uma cor vira uma declaração. Qual das duas você escreveu é o que decide, e quem decide é o renderizador — um programa não consegue pedir uma classe. Esse é o custo inteiro, e vale nomear: uma view feita só de papéis desenha em qualquer lugar, e essas duas chamadas são o que gasta isso. Use-as quando a cor vem do valor — uma por usuário, por categoria, por série — e use um papel quando significa bom, arriscado ou discreto, porque o papel é o único dos dois que um tema consegue reestilizar. Uma cor é conferida, não confiada, no caminho: um hex que não é um hex vira Normal, e não texto contrabandeado para dentro de uma folha de estilo.

É por isso também que as enumerações são tipos, e não atoms. Um atom funcionaria, e leria melhor — mas a tabela de atoms é do seu programa, numerada por ordem de primeira menção, e uma biblioteca que somasse à tabela renumeraria os atoms que você escreveu. Um conjunto fechado de variantes custa alguns caracteres e não tira nada de você.

Todos os dezessete estão no próximo passo, junto com a regra que decidiu o número: um widget só está lá se não puder ser composto a partir dos outros. Um card é uma column com padding; um toast é um overlay e uma mensagem. O link é o fora da curva, e ele ganha o lugar por ser o único que funciona sem evento nenhum.

152Every widget there isTodos os widgets que existem

Seventeen, and the rule that decided the number is worth more than the list: a widget is here only if it cannot be composed from the others and needs something the renderer has that Hive does not. A card is a padded column, a toast is an overlay and a message, a divider is a row with a border — all funcs you write, which is also the proof that the set is enough.

widgetpayloadis
row(attrs, children)View[]children laid out across
column(attrs, children)View[]children laid out down
spacer()the gap that takes whatever is left
overlay(attrs, child)Viewdrawn above everything, with a backdrop
text(attrs, content)Strthe one true leaf
image(attrs, src, alt)Str, Stra picture, described
icon(attrs, name)Iconone of a closed set of eighteen
link(attrs, href, label)Str, Stra destination, which needs no event to work
button(attrs, label)Str
input(attrs, value)Strone line; kind says which sort
textarea(attrs, value)Strseveral lines
checkbox(attrs, label, checked)Str, Boollabel included, and clickable
select(attrs, options, chosen)Str[], Strone of a few
table(attrs, rows)Tablethe headered Table everything else hands back
scene(attrs, shapes)Shape[]three dimensions
spinner(attrs)
none()nothing — and it keeps its place among its siblings

A few of them are worth a sentence each.

table takes the headered Table that a CSV read with using, a hive.sql result and a hive.json document all arrive as — the same shape Parts XII and XIII spent their time on — so putting data on the screen is one call rather than a loop.

none() draws nothing and keeps its place among its siblings, which is what makes a branch inside a view harmless. A func that returns ui.none() when there is nothing to show is an ordinary if, and the row around it does not reshuffle to make up for it.

image and link take their description and their destination as parameters rather than attributes. An attribute is optional by construction, and a picture nobody can describe or a link with nowhere to go is not a thing to make easy to write.

spacer() and none() are the only two calls in the module that take nothing at all: there is nothing to configure about a gap, or about nothing.

// Ten cells, however many of them are left. A bar is a row of small boxes —
// the sort of thing the widget set is meant to be enough for.
func bar(health: Int): ui.View {
	mut ui.View[dyn] cells = []
	for i := 0; i < 10; i++ {
		append(cells, ui.text(
			[ui.width(10), ui.height(12), ui.background(shade(health, i))],
			" "
		))
	}
	return ui.row([ui.gap(2), ui.align(ui.Align.Center())], cells)
}

And a whole heads-up display, which is one row and nothing else. ui.on is the message a click carries, and the next step is about it:

func hud(model: Model): ui.View {
	return ui.row([ui.pad(10), ui.gap(14), ui.align(ui.Align.Center())], [
		ui.text([ui.size(ui.TextSize.Subtitle()), ui.heading(1)], model.me),
		bar(model.body.health),
		ui.spacer(),                                   // takes whatever is left
		ui.text([ui.tone(ui.Tone.Warn())], "{len(mob(model))} up"),
		ui.icon([ui.tone(ui.Tone.Good())], ui.Icon.Star()),
		ui.button([ui.on(Msg.Left()), ui.tone(ui.Tone.Danger())], "Leave")
	])
}

Dezessete, e a regra que decidiu o número vale mais que a lista: um widget só está aqui se não puder ser composto a partir dos outros e precisar de algo que o renderizador tem e o Hive não. Um card é uma column com padding, um toast é um overlay e uma mensagem, um divisor é uma row com borda — todos funcs que você escreve, o que também é a prova de que o conjunto basta.

widgetcargaé
row(attrs, children)View[]filhos dispostos ao longo
column(attrs, children)View[]filhos dispostos para baixo
spacer()o vão que fica com o que sobrou
overlay(attrs, child)Viewdesenhado acima de tudo, com um fundo
text(attrs, content)Stra única folha de verdade
image(attrs, src, alt)Str, Struma imagem, descrita
icon(attrs, name)Iconum de um conjunto fechado de dezoito
link(attrs, href, label)Str, Strum destino, que funciona sem evento nenhum
button(attrs, label)Str
input(attrs, value)Struma linha; o kind diz de que tipo
textarea(attrs, value)Strvárias linhas
checkbox(attrs, label, checked)Str, Boolcom o rótulo incluído, e clicável
select(attrs, options, chosen)Str[], Strum entre alguns
table(attrs, rows)Tablea Table com cabeçalho que todo o resto devolve
scene(attrs, shapes)Shape[]três dimensões
spinner(attrs)
none()nada — e ele guarda o lugar dele entre os irmãos

Alguns deles merecem uma frase cada.

O table recebe a Table com cabeçalho que um CSV lido com using, um resultado de hive.sql e um documento de hive.json todos entregam — o mesmo formato em que as Partes XII e XIII passaram o tempo delas — então colocar dados na tela é uma chamada, não um laço.

O none() não desenha nada e guarda o lugar dele entre os irmãos, o que é o que torna um ramo dentro de uma view inofensivo. Um func que devolve ui.none() quando não há o que mostrar é um if comum, e a linha em volta não se reorganiza para compensar.

O image e o link recebem a descrição e o destino como parâmetros, não como atributos. Um atributo é opcional por construção, e uma imagem que ninguém consegue descrever ou um link sem para onde ir não são coisas que se deva facilitar escrever.

O spacer() e o none() são as duas únicas chamadas do módulo que não recebem nada: não há o que configurar num vão, nem em nada.

// Dez células, quantas ainda restarem. Uma barra é uma linha de caixinhas — o
// tipo de coisa para a qual o conjunto de widgets deve bastar.
func bar(health: Int): ui.View {
	mut ui.View[dyn] cells = []
	for i := 0; i < 10; i++ {
		append(cells, ui.text(
			[ui.width(10), ui.height(12), ui.background(shade(health, i))],
			" "
		))
	}
	return ui.row([ui.gap(2), ui.align(ui.Align.Center())], cells)
}

E um HUD inteiro, que é uma linha e mais nada. O ui.on é a mensagem que um clique carrega, e o próximo passo é sobre ele:

func hud(model: Model): ui.View {
	return ui.row([ui.pad(10), ui.gap(14), ui.align(ui.Align.Center())], [
		ui.text([ui.size(ui.TextSize.Subtitle()), ui.heading(1)], model.me),
		bar(model.body.health),
		ui.spacer(),                                   // fica com o que sobrou
		ui.text([ui.tone(ui.Tone.Warn())], "{len(mob(model))} de pé"),
		ui.icon([ui.tone(ui.Tone.Good())], ui.Icon.Star()),
		ui.button([ui.on(Msg.Left()), ui.tone(ui.Tone.Danger())], "Sair")
	])
}

153What the user did comes back as a messageO que a pessoa fez volta como mensagem

Nothing in a view holds a callback that acts — a view is a func, and step 150 said why. What an event attribute carries is a message: the same kind of value the fold in step 134 reads off a mailbox.

on and onDismiss carry that message outright. Every other event carries a function of what the user did, which is exactly what the constructor with a hole from step 61 is for.

ui.button([ui.on(Msg.Sent())], "Send")

ui.input([ui.onInput(Msg.DraftTyped(_)), ui.onSubmit(sentOf),
	ui.placeholder("Say something"), ui.grow(1)], model.draft)

ui.checkbox([ui.onToggle(Msg.Showed(_))], "Show the dead", model.showing)
ui.select([ui.onChoose(Msg.Spanned(_))], ["Day", "Week", "Month"], model.span)
ui.table([ui.onPick(Msg.Opened(_)), ui.onSort(Msg.Sorted(_))], model.rows)
attributecarries
on(Msg) onDismiss(Msg)the message itself — a click, and a click on the backdrop outside an overlay
onInput(f) onSubmit(f) onChoose(f)func(Str): Msg — what is in the field now, or what was chosen
onToggle(f)func(Bool): Msg — whether it is ticked now
onPick(f) onSort(f)func(Int): Msg — the row that was clicked, the column that was

Msg.DraftTyped(_) is a value rather than a call, and sentOf above is an ordinary func(Str): Msg — which is what you write when the message does not care what was typed. Both are just functions, so neither is a special form.

The field is not the state. An input is handed model.draft and reports what was typed; what it shows next is whatever the model says. That is one truth instead of two, and it is why "empty the box after sending" is a line in the fold rather than something to remember.

disabled(Bool) and busy(Bool) are the two attributes for work in flight, and hint(Str) is the description a control gets when its label is an icon. A button that must not be pressed twice says so from the state:

ui.button([ui.on(Msg.Sent()), ui.disabled(model.draft == ""),
	ui.busy(model.sending)], "Post")
Keep in mind

The whole view is re-rendered on every turn of the fold and swapped in when it came out different, with the focus and the caret preserved — which is what makes re-rendering everything on every keystroke a workable idea rather than a brave one. A turn that changed nothing visible sends nothing at all.

Nada numa view guarda um callback que age — uma view é um func, e o passo 150 disse por quê. O que um atributo de evento carrega é uma mensagem: o mesmo tipo de valor que o fold do passo 134 lê de uma caixa de entrada.

O on e o onDismiss carregam essa mensagem direto. Todo outro evento carrega uma função do que a pessoa fez, que é exatamente para isso que serve o construtor com buraco do passo 61.

ui.button([ui.on(Msg.Sent())], "Enviar")

ui.input([ui.onInput(Msg.DraftTyped(_)), ui.onSubmit(sentOf),
	ui.placeholder("Diga alguma coisa"), ui.grow(1)], model.draft)

ui.checkbox([ui.onToggle(Msg.Showed(_))], "Mostrar os mortos", model.showing)
ui.select([ui.onChoose(Msg.Spanned(_))], ["Dia", "Semana", "Mês"], model.span)
ui.table([ui.onPick(Msg.Opened(_)), ui.onSort(Msg.Sorted(_))], model.rows)
atributocarrega
on(Msg) onDismiss(Msg)a própria mensagem — um clique, e um clique no fundo fora de um overlay
onInput(f) onSubmit(f) onChoose(f)func(Str): Msg — o que está no campo agora, ou o que foi escolhido
onToggle(f)func(Bool): Msg — se está marcado agora
onPick(f) onSort(f)func(Int): Msg — a linha que foi clicada, a coluna que foi

Msg.DraftTyped(_) é um valor, não uma chamada, e o sentOf acima é um func(Str): Msg comum — que é o que você escreve quando a mensagem não se importa com o que foi digitado. Os dois são só funções, então nenhum dos dois é forma especial.

O campo não é o estado. Um input recebe o model.draft e reporta o que foi digitado; o que ele mostra em seguida é o que o modelo disser. Isso é uma verdade em vez de duas, e é por isso que "limpar a caixa depois de enviar" é uma linha no fold, não algo para lembrar.

O disabled(Bool) e o busy(Bool) são os dois atributos para trabalho em curso, e o hint(Str) é a descrição que um controle ganha quando o rótulo dele é um ícone. Um botão que não deve ser apertado duas vezes diz isso a partir do estado:

ui.button([ui.on(Msg.Sent()), ui.disabled(model.draft == ""),
	ui.busy(model.sending)], "Publicar")
Guarde isto

A view inteira é redesenhada a cada turno do fold e trocada quando sai diferente, com o foco e o cursor preservados — o que é o que torna redesenhar tudo a cada tecla uma ideia viável, e não uma ideia corajosa. Um turno que não mudou nada visível não envia nada.

154Seven closed setsSete conjuntos fechados

Step 151 said why the module's tokens are types rather than atoms. These are all of them. Each is a closed set, so a renderer can answer for the whole of it, and each is reached the way any type of your own is: ui.Tone.Danger().

typevariants
AlignStart Center End Stretch
JustifyStart Center End Between
TextSizeTitle Subtitle Body Caption
ToneNormal Muted Good Warn Danger HEX(Str) RGBA(Int, Int, Int, Int)
AxisHorizontal Vertical Both
InputKindText Number Date Password Search
IconSearch Close Check Plus Minus Up Down Left Right Warning Info Star Heart Reply Repeat Trash User Menu

Two of these carry more than they look like they do.

size and heading are not the same thing, and a title is usually both. size(ui.TextSize.Title()) is how big it looks; heading(1) is what it is — a level from 1 to 6, which a served page renders as a real heading and which tells anyone reading with their ears what the shape of the page is. Text can be large without being a heading, and a heading can be small.

kind is what an input is for: Number asks a phone for the right keyboard, Password hides what is typed, Date and Search get whatever control the platform already has. It says nothing about the value, which is a Str either way — turning one into a number is hive.conv's job from step 116, and it can fail.

ui.column([ui.gap(8), ui.align(ui.Align.Stretch()), ui.scroll(ui.Axis.Vertical())], [
	ui.text([ui.size(ui.TextSize.Title()), ui.heading(1)], "Sign in"),
	ui.input([ui.kind(ui.InputKind.Password()), ui.onInput(Msg.Typed(_))], model.secret),
	ui.row([ui.justify(ui.Justify.Between())], [
		ui.link([], "/forgot", "Forgotten it?"),
		ui.button([ui.on(Msg.SignedIn())], "Go")
	])
])

align and justify are the two directions of a row or a column: justify works along it, align across it. And scroll is where the overflow may go — a chat log is a column with scroll(ui.Axis.Vertical()) and grow(1), which is the whole of it.

O passo 151 disse por que os tokens do módulo são tipos e não átomos. Estes são todos eles. Cada um é um conjunto fechado, para que um renderizador possa responder pelo conjunto inteiro, e cada um é alcançado como qualquer tipo seu: ui.Tone.Danger().

tipovariantes
AlignStart Center End Stretch
JustifyStart Center End Between
TextSizeTitle Subtitle Body Caption
ToneNormal Muted Good Warn Danger HEX(Str) RGBA(Int, Int, Int, Int)
AxisHorizontal Vertical Both
InputKindText Number Date Password Search
IconSearch Close Check Plus Minus Up Down Left Right Warning Info Star Heart Reply Repeat Trash User Menu

Dois deles carregam mais do que parecem.

size e heading não são a mesma coisa, e um título normalmente é os dois. size(ui.TextSize.Title()) é o tamanho que ele aparenta; heading(1) é o que ele é — um nível de 1 a 6, que uma página servida renderiza como cabeçalho de verdade e que diz a quem lê de ouvido qual é o formato da página. Texto pode ser grande sem ser cabeçalho, e um cabeçalho pode ser pequeno.

O kind é para que serve um input: Number pede o teclado certo num celular, Password esconde o que é digitado, Date e Search pegam o controle que a plataforma já tem. Ele não diz nada sobre o valor, que é um Str de todo jeito — transformar um em número é trabalho do hive.conv do passo 116, e pode falhar.

ui.column([ui.gap(8), ui.align(ui.Align.Stretch()), ui.scroll(ui.Axis.Vertical())], [
	ui.text([ui.size(ui.TextSize.Title()), ui.heading(1)], "Entrar"),
	ui.input([ui.kind(ui.InputKind.Password()), ui.onInput(Msg.Typed(_))], model.secret),
	ui.row([ui.justify(ui.Justify.Between())], [
		ui.link([], "/esqueci", "Esqueceu?"),
		ui.button([ui.on(Msg.SignedIn())], "Vai")
	])
])

align e justify são as duas direções de uma row ou de uma column: o justify age ao longo dela, o align atravessado. E o scroll é para onde o excedente pode ir — um histórico de conversa é uma column com scroll(ui.Axis.Vertical()) e grow(1), e é só isso.

155A window is a serviceUma janela é um serviço

Showing a view takes four things: a title, the view, an update, and the state to start from.

proc main(): void {
	ui.window("Orders", view, update, Model([], false))
}

And update is not a new idea. It is the same fold over a mailbox that Part XV's services are — proc (State, Message, hive.syslink.Envelope): State — and it is checked as one, because a window is a service:

proc update(model: Model, msg: Msg, from: hive.syslink.Envelope): Model {
	if msg is Msg.Refreshed {
		return Model(model.rows, true)
	}
	if msg is Msg.Loaded(rows) {
		return Model(rows, false)
	}
	return model
}

Which buys three things at once. There is no mutex anywhere in a Hive interface — the fold is the mutex, as it was in step 134. The window has an address, so anything at all can post to it. And every rule you already learned about a service applies unchanged.

What opening one does, and what closing it does

window binds a port the operating system chooses, on the loopback interface only, puts a random token in the URL, and starts a browser already installed on the machine in application mode. Nothing is linked into your binary for it: each browser is a program that may or may not be there, which is what keeps a window free of build dependencies on every platform. Failing all of them it falls back to an ordinary tab, and failing that it prints the URL.

And window does not return, because closing the window ends the program. There is nothing left to serve once the last page has gone, and a process that outlived its window would be one nobody can see, nobody can reach and nobody thought to stop — still holding a port and still answering as a node. A page being reloaded is not a window being closed, and the difference is a second and a half.

Mostrar uma view exige quatro coisas: um título, a view, um update, e o estado inicial.

proc main(): void {
	ui.window("Pedidos", view, update, Model([], false))
}

E update não é uma ideia nova. É o mesmo fold sobre uma caixa de mensagens que os serviços da Parte XV são — proc (State, Message, hive.syslink.Envelope): State — e é verificado como um, porque uma janela é um serviço:

proc update(model: Model, msg: Msg, from: hive.syslink.Envelope): Model {
	if msg is Msg.Refreshed {
		return Model(model.rows, true)
	}
	if msg is Msg.Loaded(rows) {
		return Model(rows, false)
	}
	return model
}

O que rende três coisas de uma vez. Não existe mutex nenhum numa interface Hive — o fold é o mutex, como era no passo 134. A janela tem um endereço, então qualquer coisa pode mandar mensagem para ela. E toda regra que você já aprendeu sobre serviços vale sem mudança.

O que abrir uma faz, e o que fechar uma faz

O window ocupa uma porta escolhida pelo sistema operacional, só na interface de loopback, põe um token aleatório na URL, e inicia um navegador já instalado na máquina em modo aplicação. Nada é ligado ao seu binário por causa disso: cada navegador é um programa que pode ou não estar lá, e é o que mantém uma janela livre de dependências de build em toda plataforma. Se nenhum deles funcionar, ele cai para uma aba comum, e se isso também falhar, imprime a URL.

E o window não retorna, porque fechar a janela encerra o programa. Não sobra nada para servir depois que a última página foi embora, e um processo que sobrevivesse à própria janela seria um que ninguém vê, ninguém alcança e ninguém pensou em parar — ainda ocupando uma porta e ainda respondendo como nó. Uma página sendo recarregada não é uma janela sendo fechada, e a diferença é um segundo e meio.

156There is no command typeNão existe um tipo de comando

Every language built this way needs an answer to "the update wants to fetch something, and must not block the screen while it does". Most of them invent a type for it — a Cmd, a task, an effect the runtime interprets.

Hive needs none, because it already has two things that add up to one. update is a proc, so it may act. And the window has an address, which step 155 just gave it.

if msg is Msg.Refreshed {
	async reload(hive.syslink.self(from))    // off it goes; nothing to hold
	return Model(model.rows, true)           // ...and the spinner starts now
}

if msg is Msg.Loaded(rows) {                 // it comes back as a message
	return Model(rows, false)
}
proc reload(window: hive.syslink.Address): void {
	if using "./orders.csv" is Result.Ok(rows) {
		async window(Msg.Loaded(rows))
	}
}

async from step 89, an address from step 136, and a message you already declared. The work runs on its own thread, the fold is never blocked, and the answer arrives the same way a click does — so there is exactly one way into the state, and it is the one you have been reading all along.

The same door is open from further away. A timer, another service, or a node on another machine can post to that address, and the screen updates. Nothing about the view or the fold changes to allow it.

Toda linguagem construída assim precisa de uma resposta para "o update quer buscar algo, e não pode travar a tela enquanto isso". A maioria inventa um tipo para isso — um Cmd, uma task, um efeito que o runtime interpreta.

Hive não precisa de nenhum, porque já tem duas coisas que somam a isso. update é um proc, então pode agir. E a janela tem um endereço, que o passo 155 acabou de dar a ela.

if msg is Msg.Refreshed {
	async reload(hive.syslink.self(from))    // lá se foi; nada para segurar
	return Model(model.rows, true)           // ...e o spinner começa agora
}

if msg is Msg.Loaded(rows) {                 // volta como uma mensagem
	return Model(rows, false)
}
proc reload(window: hive.syslink.Address): void {
	if using "./orders.csv" is Result.Ok(rows) {
		async window(Msg.Loaded(rows))
	}
}

async do passo 89, um endereço do passo 136, e uma mensagem que você já declarou. O trabalho roda na própria thread, o fold nunca trava, e a resposta chega do mesmo jeito que um clique — então existe exatamente um caminho até o estado, e é o que você vem lendo desde o começo.

A mesma porta está aberta de mais longe. Um timer, outro serviço, ou um nó em outra máquina pode mandar mensagem para esse endereço, e a tela atualiza. Nada na view nem no fold muda para permitir isso.

157The same view, servedA mesma view, servida

A view is a value, so showing it in a window is one thing you can do with it and not the only one. ui.page turns the identical tree into a string of HTML:

proc page(request: web.HttpRequest): web.HttpResponse {
	return web.HttpResponse(
		200,
		[["Content-Type", "text/html; charset=utf-8"]],
		ui.page("Orders", view(model))
	)
}

That is not a second view written to match the first. It is view, called the way the window calls it, with a string coming back instead of a window going up. ui.html does the same for a fragment, when the page around it is yours.

A served page has nowhere to send a click back to, so the renderer writes no handler ids at all — the messages in the tree are simply not recorded. That is the honest rendering of a page with no socket behind it, rather than a page that looks interactive and is not.

Which is exactly what ui.link is for. It is the one widget that works without an event — an anchor needs no socket, no handler and no state — and it can carry a message as well as a destination:

ui.link([ui.on(Msg.Went(Route.Explore()))], "/explore", "Explore")

The window follows the message; the served page follows the href; both from the identical tree. So one nav rail works in either, and a page with no socket behind it is still a page you can move around in. Where a link may point is a closed set — somewhere within the page, a relative path, or http, https, mailto — so a URL that is executable rather than a destination renders as no destination at all.

The window itself is a browser already on the machine, started in application mode — a real window, no address bar, no tabs. Nothing is linked into your binary for it, so a program with a window still builds offline into one executable with no dependencies, on Linux and on Windows alike.

Uma view é um valor, então mostrá-la numa janela é uma coisa que dá para fazer com ela, e não a única. ui.page transforma a árvore idêntica numa string de HTML:

proc page(request: web.HttpRequest): web.HttpResponse {
	return web.HttpResponse(
		200,
		[["Content-Type", "text/html; charset=utf-8"]],
		ui.page("Pedidos", view(model))
	)
}

Isso não é uma segunda view escrita para combinar com a primeira. É a view, chamada do jeito que a janela a chama, com uma string voltando em vez de uma janela subindo. ui.html faz o mesmo para um fragmento, quando a página ao redor é sua.

Uma página servida não tem para onde mandar um clique de volta, então o renderizador não escreve id de handler nenhum — as mensagens da árvore simplesmente não são registradas. Essa é a renderização honesta de uma página sem socket atrás dela, em vez de uma página que parece interativa e não é.

E é exatamente para isso que existe o ui.link. É o único widget que funciona sem evento — uma âncora não precisa de socket, de handler nem de estado — e ele pode carregar uma mensagem além de um destino:

ui.link([ui.on(Msg.Went(Route.Explore()))], "/explore", "Explore")

A janela segue a mensagem; a página servida segue o href; as duas a partir da árvore idêntica. Então uma mesma barra de navegação funciona nos dois casos, e uma página sem socket atrás dela ainda é uma página por onde dá para andar. Para onde um link pode apontar é um conjunto fechado — algum lugar dentro da página, um caminho relativo, ou http, https, mailto — então uma URL que é executável em vez de um destino é renderizada sem destino nenhum.

A janela em si é um navegador que já está na máquina, iniciado em modo aplicação — uma janela de verdade, sem barra de endereço, sem abas. Nada é linkado no seu binário por causa disso, então um programa com janela continua buildando offline num único executável sem dependências, tanto no Linux quanto no Windows.

158An interface you can testUma interface que dá para testar

This is the part that falls out for free, and the reason a view is a value rather than a template. ui.html is the same renderer a served page uses, so a test can render a view and read it — no browser, no screenshot, no snapshot file.

test "the post button is disabled until something is typed" {
	empty := ui.html(view(Model(posts, "")))
	assert indexOf(empty, "disabled>Post") is Result.Ok(_)

	typed := ui.html(view(Model(posts, "hi")))
	assert indexOf(typed, "disabled>Post") is Result.Error(_)
}

And the one worth writing first, because a post's body is somebody else's text:

test "a post body cannot smuggle markup into the page" {
	page := ui.html(view(withBody("<script>steal()</script>")))
	assert indexOf(page, "<script>") is Result.Error(_)
}

The update is testable for the same reason — it is a proc taking a state and returning one, so a test calls it and looks at what came back. One thing it cannot do today: an envelope is opaque and only ever arrives as a handler's own argument, so a test cannot build one and therefore cannot call update directly. Test what its branches are made of instead.

The whole thing: code-examples/17 - EXAMPLE APP - Chat with User Interface/ — two peers, each a window you talk from and a mailbox the other one posts to. There is no server: the window publishes itself under a name, so the far node reaches the screen by calling an address. No wire format is written down anywhere — a message is a value and the compiler derives the codec. With a suite of its own, including a real node on a real port.

Essa é a parte que sai de graça, e a razão de uma view ser um valor e não um template. ui.html é o mesmo renderizador que uma página servida usa, então um teste pode renderizar uma view e ler o resultado — sem navegador, sem screenshot, sem arquivo de snapshot.

test "o botão de postar fica desabilitado até digitarem algo" {
	empty := ui.html(view(Model(posts, "")))
	assert indexOf(empty, "disabled>Post") is Result.Ok(_)

	typed := ui.html(view(Model(posts, "oi")))
	assert indexOf(typed, "disabled>Post") is Result.Error(_)
}

E o que vale escrever primeiro, porque o corpo de um post é texto de outra pessoa:

test "o corpo de um post não contrabandeia markup para a página" {
	page := ui.html(view(withBody("<script>steal()</script>")))
	assert indexOf(page, "<script>") is Result.Error(_)
}

O update é testável pelo mesmo motivo — é um proc que recebe um estado e devolve um, então um teste o chama e olha o que voltou. Uma coisa que ainda não dá: um envelope é opaco e só chega como argumento do próprio handler, então um teste não consegue construir um e portanto não consegue chamar o update direto. Teste do que os ramos dele são feitos.

A coisa toda: code-examples/17 - EXAMPLE APP - Chat with User Interface/ — dois pares, cada um uma janela de onde você fala e uma caixa de mensagens em que o outro posta. Não há servidor: a janela publica a si mesma sob um nome, então o nó distante alcança a tela chamando um endereço. Nenhum formato de fio é escrito em lugar nenhum — uma mensagem é um valor e o compilador deriva o codec. Com uma suíte própria, incluindo um nó de verdade numa porta de verdade.

159Three dimensions: ui.sceneTrês dimensões: ui.scene

scene(attrs, shapes) is a widget like any other — attributes, then a payload — and it draws a world in the box it occupies. The payload is a vector of ui.Shape, which is a different type from View on purpose: a box in a column and a button in a scene are both mistakes, and two closed sets are what make them mistakes the compiler catches rather than things a renderer quietly drops.

What a scene is not is a canvas. There is no drawing context to hold, no mesh to keep, nothing to add to and nothing to free. A scene is a value, exactly like the panel around it: an ordinary func answers "what is there now" from the state, sixty times a second, and the renderer works out what changed.

func view(model: Model): ui.View {
	return ui.column([ui.height(0), ui.grow(1)], [
		hud(model),
		ui.scene(
			[
				ui.grow(1),
				ui.eye(model.body.x, model.body.y + eyes(), model.body.z),
				ui.aim(model.body.yaw, model.body.pitch),
				ui.lens(78),
				ui.fog(70),
				ui.background(ui.Tone.HEX("#8ecbff")),   // the sky
				ui.onFrame(Msg.Tick(_))
			],
			world(model)
		),
		panel(model)
	])
}

And world is a func that builds a vector. There is nothing three-dimensional about the way it is written — it is the loop and the append from Part IV:

// Sixty of these are built a second and thrown away; if drawing could act,
// every frame would act again.
func world(model: Model): ui.Shape[dyn] {
	mut ui.Shape[dyn] shapes = [
		ui.ground([ui.paint(ui.Tone.HEX("#5f9e4a"))], 44.0, 44.0)
	]
	for each block in arena() {
		append(shapes, ui.box(
			[ui.at(block.x, block.y, block.z), ui.paint(ui.Tone.HEX(block.shade))],
			block.w, block.h, block.d
		))
	}
	for each foe in model.others {
		for each piece in player(foe) {
			append(shapes, piece)
		}
	}
	return shapes
}

Which is the whole model, and it is why a world can be inspected without a window: len(world(model)) is an assertion, and step 164 is about that.

Convention

Distances are in whatever unit you decide 1.0 is — a metre, in everything that follows. Angles are in radians, which is what hive.math speaks.

scene(attrs, shapes) é um widget como qualquer outro — atributos, depois uma carga — e desenha um mundo na caixa que ele ocupa. A carga é um vetor de ui.Shape, que é um tipo diferente de View de propósito: uma caixa dentro de uma column e um botão dentro de uma cena são os dois erros, e dois conjuntos fechados são o que faz deles erros que o compilador apanha, em vez de coisas que o renderizador descarta em silêncio.

O que uma cena não é: um canvas. Não há contexto de desenho para segurar, nem mesh para guardar, nada para acrescentar e nada para liberar. Uma cena é um valor, exatamente como o painel em volta dela: um func comum responde "o que está aí agora" a partir do estado, sessenta vezes por segundo, e o renderizador descobre o que mudou.

func view(model: Model): ui.View {
	return ui.column([ui.height(0), ui.grow(1)], [
		hud(model),
		ui.scene(
			[
				ui.grow(1),
				ui.eye(model.body.x, model.body.y + eyes(), model.body.z),
				ui.aim(model.body.yaw, model.body.pitch),
				ui.lens(78),
				ui.fog(70),
				ui.background(ui.Tone.HEX("#8ecbff")),   // o céu
				ui.onFrame(Msg.Tick(_))
			],
			world(model)
		),
		panel(model)
	])
}

E o world é um func que monta um vetor. Não há nada de tridimensional no jeito como ele é escrito — é o laço e o append da Parte IV:

// Sessenta destes são montados por segundo e jogados fora; se desenhar pudesse
// agir, cada quadro agiria de novo.
func world(model: Model): ui.Shape[dyn] {
	mut ui.Shape[dyn] shapes = [
		ui.ground([ui.paint(ui.Tone.HEX("#5f9e4a"))], 44.0, 44.0)
	]
	for each block in arena() {
		append(shapes, ui.box(
			[ui.at(block.x, block.y, block.z), ui.paint(ui.Tone.HEX(block.shade))],
			block.w, block.h, block.d
		))
	}
	for each foe in model.others {
		for each piece in player(foe) {
			append(shapes, piece)
		}
	}
	return shapes
}

É esse o modelo inteiro, e é por isso que um mundo pode ser examinado sem janela: len(world(model)) é uma asserção, e o passo 164 é sobre isso.

Convenção

Distâncias estão na unidade que você decidir que 1.0 é — um metro, em tudo o que vem a seguir. Ângulos estão em radianos, que é o que o hive.math fala.

160The six shapesAs seis formas

Six, chosen by the rule that chose the widgets: a shape is here only if it cannot be built out of the others. A crate is a box, a barrel is a cylinder, a player is a box and a sphere, a tracer is a line — all funcs you write, which is the proof that six is enough.

shapepayloadis
box(attrs, width, height, depth)three Floats
sphere(attrs, radius)Float
cylinder(attrs, radius, height)Float, Float
ground(attrs, width, depth)Float, Floata plane that lies flat without being turned
label(attrs, words)Strwords in the world, always facing whoever is looking
line(attrs, fromX, fromY, fromZ, toX, toY, toZ)six Floatsthe one shape that carries both its ends

Dimensions are the payload; where it stands is an attribute. That split is not arbitrary: a sphere without a radius is not a sphere with a default one, while a shape at the origin facing forward is a perfectly ordinary thing to want. So the three attributes a shape takes are at(x, y, z), turn(x, y, z) and paint(Tone) — and the two exceptions in the table fall out of the same reasoning. A label takes no turn, because one you could only read from one side would not be a label; a line takes no at, because a line is between two places and has no third one of its own.

paint takes the same Tone everything else does. The five roles answer with a fixed colour in a world — there is no stylesheet in a scene — and HEX and RGBA are for a colour that is genuinely data, one per player:

// Somebody else: a body, a head, a snout to show which way they are facing, and
// their name above them. Four shapes, and a colour computed from their endpoint.
func player(foe: Foe): ui.Shape[dyn] {
	shade := colour(foe.endpoint)
	return [
		ui.box([ui.at(foe.x, foe.y + 0.62, foe.z), ui.turn(0.0, foe.yaw, 0.0),
			ui.paint(shade)], 0.56, 1.24, 0.34),
		ui.sphere([ui.at(foe.x, foe.y + 1.48, foe.z),
			ui.paint(ui.Tone.HEX("#f2d3ae"))], 0.22),
		ui.box([ui.at(foe.x - math.sin(foe.yaw) * 0.42, foe.y + 1.2,
			foe.z - math.cos(foe.yaw) * 0.42), ui.turn(0.0, foe.yaw, 0.0),
			ui.paint(ui.Tone.HEX("#2b2f36"))], 0.12, 0.12, 0.5),
		ui.label([ui.at(foe.x, foe.y + 2.1, foe.z),
			ui.paint(ui.Tone.HEX("#ffffff"))], foe.name)
	]
}

A shape is a value and a world is a vector of them, so everything Part IV taught applies here without a word of adjustment: append adds one, len counts them, filter keeps some. A "model" in this module is not a file you load — it is a func returning a few of these.

Seis, escolhidas pela mesma regra que escolheu os widgets: uma forma só está aqui se não puder ser construída a partir das outras. Um engradado é um box, um barril é um cylinder, um jogador é uma caixa e uma esfera, um rastro de bala é uma line — todos funcs que você escreve, o que é a prova de que seis bastam.

formacargaé
box(attrs, width, height, depth)três Floats
sphere(attrs, radius)Float
cylinder(attrs, radius, height)Float, Float
ground(attrs, width, depth)Float, Floatum plano que fica deitado sem precisar ser virado
label(attrs, words)Strpalavras no mundo, sempre viradas para quem olha
line(attrs, fromX, fromY, fromZ, toX, toY, toZ)seis Floatsa única forma que carrega as duas pontas

As dimensões são a carga; onde ela está é atributo. Essa divisão não é arbitrária: uma esfera sem raio não é uma esfera com raio padrão, enquanto uma forma na origem virada para frente é uma coisa perfeitamente comum de se querer. Então os três atributos que uma forma recebe são at(x, y, z), turn(x, y, z) e paint(Tone) — e as duas exceções da tabela saem do mesmo raciocínio. Um label não recebe turn, porque um que só se pudesse ler de um lado não seria um label; uma line não recebe at, porque uma linha está entre dois lugares e não tem um terceiro só dela.

O paint recebe o mesmo Tone que todo o resto. Os cinco papéis respondem com uma cor fixa dentro de um mundo — não existe folha de estilo numa cena — e o HEX e o RGBA são para uma cor que é genuinamente dado, uma por jogador:

// Outra pessoa: um corpo, uma cabeça, um bico para mostrar para onde ela está
// virada, e o nome dela acima. Quatro formas, e uma cor calculada do endpoint.
func player(foe: Foe): ui.Shape[dyn] {
	shade := colour(foe.endpoint)
	return [
		ui.box([ui.at(foe.x, foe.y + 0.62, foe.z), ui.turn(0.0, foe.yaw, 0.0),
			ui.paint(shade)], 0.56, 1.24, 0.34),
		ui.sphere([ui.at(foe.x, foe.y + 1.48, foe.z),
			ui.paint(ui.Tone.HEX("#f2d3ae"))], 0.22),
		ui.box([ui.at(foe.x - math.sin(foe.yaw) * 0.42, foe.y + 1.2,
			foe.z - math.cos(foe.yaw) * 0.42), ui.turn(0.0, foe.yaw, 0.0),
			ui.paint(ui.Tone.HEX("#2b2f36"))], 0.12, 0.12, 0.5),
		ui.label([ui.at(foe.x, foe.y + 2.1, foe.z),
			ui.paint(ui.Tone.HEX("#ffffff"))], foe.name)
	]
}

Uma forma é um valor e um mundo é um vetor delas, então tudo o que a Parte IV ensinou se aplica aqui sem uma palavra de ajuste: append acrescenta uma, len conta, filter guarda algumas. Um "modelo" neste módulo não é um arquivo que você carrega — é um func devolvendo algumas destas.

161Where the eye is, and which way is forwardOnde está o olho, e qual direção é para frente

There is exactly one camera per scene, so where the eye is and what it can see are attributes — configuration rather than content. Every one of them is read straight out of the state, and that is the whole idea: the camera is not something the program owns, it is something the program describes.

attributeis
eye(x, y, z)where you are
aim(yaw, pitch)where you are looking, in radians
lens(Int)the field of view, in degrees
fog(Int)the distance at which the sky swallows things
background(Tone)the sky
grab(Bool) crosshair(Bool)the mouse — step 162

grow, width and height mean on a scene what they mean on any widget, which is how it gets its box. Everything else in the attribute vocabulary means nothing here and is ignored, the way pad means nothing on a spacer: an attribute is a token the renderer may or may not have a use for.

Which way is forward. aim(0.0, 0.0) looks down −Z, with +X to the right and +Y up. Yaw turns about the vertical and pitch tilts, so forward is (-sin(yaw), -cos(yaw)) and right is (cos(yaw), -sin(yaw)) — two hive.math calls, and the only convention you have to hold on to.

Existe exatamente uma câmera por cena, então onde o olho está e o que ele alcança são atributos — configuração, não conteúdo. Cada um deles é lido direto do estado, e essa é a ideia inteira: a câmera não é algo que o programa possui, é algo que o programa descreve.

atributoé
eye(x, y, z)onde você está
aim(yaw, pitch)para onde você olha, em radianos
lens(Int)o campo de visão, em graus
fog(Int)a distância em que o céu engole as coisas
background(Tone)o céu
grab(Bool) crosshair(Bool)o mouse — passo 162

O grow, o width e o height significam numa cena o que significam em qualquer widget, e é assim que ela ganha a caixa dela. Todo o resto do vocabulário de atributos não significa nada aqui e é ignorado, do mesmo jeito que pad não significa nada num spacer: um atributo é um token para o qual o renderizador pode ou não ter uso.

Qual direção é para frente. aim(0.0, 0.0) olha para −Z, com +X à direita e +Y para cima. O yaw gira em torno da vertical e o pitch inclina, então para frente é (-sin(yaw), -cos(yaw)) e para a direita é (cos(yaw), -sin(yaw)) — duas chamadas de hive.math, e a única convenção que você precisa guardar.

The world from outside, which is not where the camera is: the teal marker is eye, and the line leaving it is aim. Every solid in it is one of the six shapes. Drag to turn the whole thing — the camera turns with it, because it is a value in the world like everything else. O mundo visto de fora, que não é de onde a câmera olha: o marcador azul-esverdeado é o eye, e a linha que sai dele é o aim. Cada sólido ali é uma das seis formas. Arraste para girar tudo — a câmera gira junto, porque ela é um valor no mundo como qualquer outro.

aim(0.0, 0.0) looks down −Z with +X to the right and +Y up. A yaw of −π/2 is a quarter turn clockwise seen from above, so forward becomes +X; a positive pitch tilts the look upwards. eye is a place, and putting it 1.62 metres above a body is what makes the picture first-person.

aim(0.0, 0.0) olha para −Z, com +X à direita e +Y para cima. Um yaw de −π/2 é um quarto de volta no sentido do relógio visto de cima, então "para frente" passa a ser +X; um pitch positivo inclina o olhar para cima. O eye é um lugar, e colocá-lo 1,62 metro acima de um corpo é o que faz a imagem ser em primeira pessoa.

Walking is that convention and nothing more. The keys give a forward amount and a sideways one, and the two vectors turn them into a step:

fx := 0.0 - math.sin(body.yaw)
fz := 0.0 - math.cos(body.yaw)
mut Float dx = fx * ahead + math.cos(body.yaw) * side
mut Float dz = fz * ahead + (0.0 - math.sin(body.yaw)) * side

// Diagonals are not faster. The length is what is normalised, so holding two
// keys turns you rather than launching you.
length := math.hypot(dx, dz)
if length > 0.0 {
	step := speed() * dt / length
	dx = dx * step
	dz = dz * step
}
Three numbers, not a vector

at, turn and eye take three Floats rather than one vector, on purpose. A program's own idea of a point is its own — a struct with names, most likely — and a library type it had to convert into would be one more thing standing between the state and the picture.

Andar é essa convenção e mais nada. As teclas dão uma quantidade para frente e uma para o lado, e os dois vetores transformam isso num passo:

fx := 0.0 - math.sin(body.yaw)
fz := 0.0 - math.cos(body.yaw)
mut Float dx = fx * ahead + math.cos(body.yaw) * side
mut Float dz = fz * ahead + (0.0 - math.sin(body.yaw)) * side

// Diagonais não são mais rápidas. O que é normalizado é o comprimento, então
// segurar duas teclas te vira em vez de te lançar.
length := math.hypot(dx, dz)
if length > 0.0 {
	step := speed() * dt / length
	dx = dx * step
	dz = dz * step
}
Três números, não um vetor

O at, o turn e o eye recebem três Floats em vez de um vetor, de propósito. A ideia de ponto de um programa é dele — uma struct com nomes, provavelmente — e um tipo de biblioteca no qual ele tivesse que converter seria uma coisa a mais entre o estado e a imagem.

162A frame, a key, a lookUm quadro, uma tecla, um olhar

A scene's events are ordinary event attributes, carrying ordinary messages into the same fold. There are five of them.

ui.onFrame(Msg.Tick(_)),                 // func(Int): Msg — milliseconds
ui.onKeyDown(Msg.Pressed(_)),            // func(Str): Msg — which key
ui.onKeyUp(Msg.Released(_)),
ui.onLook(Msg.Looked(_, _)),             // func(Float, Float): Msg
ui.onGrab(Msg.Grabbed(_))                // func(Bool): Msg

onFrame reports the milliseconds since the last frame, and it is the browser's own frame: the refresh rate of the screen, stopped while the window is hidden, never twice for one paint. A gap longer than 100ms arrives as 100 — a longer one means the window was busy, and a world stepped by the real gap would teleport through its walls.

onKeyDown and onKeyUp name the key. A letter or a digit is itself, and everything else is the word for it: "space", "shift", "ctrl", "alt", "tab", "enter", "escape", "up", "down", "left", "right" — and the mouse buttons as "mouse1", "mouse2", "mouse3", because holding a trigger is the same kind of thing as holding W and arrives the same way. Nothing repeats while a key is held down, keys typed into an input are not reported at all, and everything held is released when the window loses focus.

Which is why a key event moves nobody. It writes down what is held; onFrame is what moves. That is not a trick — it is the only shape that gives you a speed instead of a step per event, and it puts the whole of "how fast do I walk" in one place:

if msg is Msg.Pressed(key) {
	return holding(model, key, true)          // written down, not acted on
}
if msg is Msg.Tick(ms) {
	return stepped(model, math.min(conv.itf(ms) / 1000.0, 0.05))
}

onLook reports how far the mouse moved across and down since the last frame, and it is the one event in the module that carries two numbers — which is exactly the shape a constructor with two holes already has: Msg.Looked(_, _).

Holding the mouse. grab(true) asks for the pointer, and a browser only grants that on a click. So while it is on, a click anywhere in the window takes the mouse — except on something the program is listening to, where a button stays a button and a field stays a field. That is wider than "click the picture" on purpose: a program that has not got the mouse yet usually says so with a panel drawn over the scene, and a panel that swallowed the very click it was asking for would be a circle with no way out. The click that takes the mouse is not also delivered as a "mouse1", for the same reason.

Escape gives the mouse back, and so does grab(false); either way onGrab says so, which is how a program knows to put its menu up. crosshair(true) draws the aiming mark in the middle of the box.

Keep in mind

All five carry messages into the fold that draws the window, so everything Part XV said still holds: a frame and a packet from another machine are the same kind of arrival, handled one at a time, with no mutex anywhere. A multiplayer game is not a different architecture from a form.

Os eventos de uma cena são atributos de evento comuns, carregando mensagens comuns para o mesmo fold. São cinco.

ui.onFrame(Msg.Tick(_)),                 // func(Int): Msg — milissegundos
ui.onKeyDown(Msg.Pressed(_)),            // func(Str): Msg — qual tecla
ui.onKeyUp(Msg.Released(_)),
ui.onLook(Msg.Looked(_, _)),             // func(Float, Float): Msg
ui.onGrab(Msg.Grabbed(_))                // func(Bool): Msg

O onFrame reporta os milissegundos desde o último quadro, e é o quadro do próprio navegador: a taxa de atualização da tela, parada enquanto a janela está escondida, nunca duas vezes para um mesmo desenho. Um intervalo maior que 100ms chega como 100 — um intervalo desse tamanho significa que a janela estava ocupada, e um mundo avançado pelo intervalo real atravessaria as próprias paredes.

O onKeyDown e o onKeyUp dizem qual tecla. Uma letra ou um dígito é o próprio símbolo, e todo o resto é a palavra para aquilo: "space", "shift", "ctrl", "alt", "tab", "enter", "escape", "up", "down", "left", "right" — e os botões do mouse como "mouse1", "mouse2", "mouse3", porque segurar um gatilho é o mesmo tipo de coisa que segurar o W e chega do mesmo jeito. Nada repete enquanto uma tecla está pressionada, teclas digitadas dentro de um input não são reportadas, e tudo que estava sendo segurado é solto quando a janela perde o foco.

É por isso que um evento de tecla não move ninguém. Ele anota o que está sendo segurado; quem move é o onFrame. Não é truque — é a única forma que te dá uma velocidade em vez de um passo por evento, e põe todo o "quão rápido eu ando" num lugar só:

if msg is Msg.Pressed(key) {
	return holding(model, key, true)           // anotada, não executada
}
if msg is Msg.Tick(ms) {
	return stepped(model, math.min(conv.itf(ms) / 1000.0, 0.05))
}

O onLook reporta quanto o mouse andou para o lado e para baixo desde o último quadro, e é o único evento do módulo que carrega dois números — que é exatamente a forma que um construtor com dois buracos já tem: Msg.Looked(_, _).

Segurar o mouse. O grab(true) pede o ponteiro, e um navegador só concede isso num clique. Então, enquanto está ligado, um clique em qualquer lugar da janela captura o mouse — exceto em algo que o programa esteja escutando, onde um botão continua botão e um campo continua campo. Isso é mais amplo que "clique na imagem" de propósito: um programa que ainda não tem o mouse normalmente diz isso com um painel desenhado sobre a cena, e um painel que engolisse justamente o clique que estava pedindo seria um círculo sem saída. O clique que captura o mouse também não é entregue como um "mouse1", pelo mesmo motivo.

Escape devolve o mouse, e o grab(false) também; de um jeito ou de outro o onGrab avisa, e é assim que o programa sabe que é hora de subir o menu. O crosshair(true) desenha a mira no meio da caixa.

Guarde isto

Todos os cinco levam mensagens para o fold que desenha a janela, então tudo o que a Parte XV disse continua valendo: um quadro e um pacote de outra máquina são o mesmo tipo de chegada, tratados um por vez, sem mutex nenhum. Um jogo multiplayer não é uma arquitetura diferente de um formulário.

163What a scene costs, and what it does notO que uma cena custa, e o que ela não custa

Rebuilding a world sixty times a second sounds expensive, and the reason it is not is worth knowing, because it is what makes the value model affordable rather than merely tidy.

A scene's contents travel on a frame of their own, separate from the document. So a world redrawn sixty times a second does not re-render the panel around it, and the canvas is never replaced. The renderer keeps a geometry per shape-and-size, a material per colour and a texture per label, and reuses the object in each slot while it still stands for the same thing — a moving world is a position written into a mesh that is already there.

Two limits are worth knowing about.

prints
hive: a window draws one scene, and this view has 2 — the first one is the one on screen

A window draws one scene. A view holding two draws the first and says so on stdout, once, rather than leaving a second picture silently blank.

A scene rendered by ui.html or ui.page is an empty box. A served page has no socket, so it has no frames to draw — exactly as a button on such a page has nowhere to send its message. The HTML around it is real; the world in it is not.

three.js is fetched at compile time, and only for a scene

A scene is drawn with three.js, and it is the only thing the compiler ever fetches for its own runtime — the database drivers of step 112, and whatever a Go or a remote import brings with it, are your program's own dependencies, resolved by the Go toolchain. The library is pinned to one version and to the SHA-256 of each of its files: a download that hashes differently fails the build rather than being used or cached, which covers a corrupted download, a tampered one, and a registry that quietly republished a version. It is fetched once, into ~/.hive/vendor, embedded into the executable, and served by the window itself.

So the program you ship is still a single file that runs on a machine with no network, and a program that draws no scene neither downloads it nor carries it. Only the first build of a scene needs the network for it.

Remontar um mundo sessenta vezes por segundo soa caro, e vale saber por que não é: é isso que torna o modelo de valores viável, e não apenas elegante.

O conteúdo de uma cena viaja num quadro próprio, separado do documento. Então um mundo redesenhado sessenta vezes por segundo não redesenha o painel em volta dele, e o canvas nunca é substituído. O renderizador guarda uma geometria por forma-e-tamanho, um material por cor e uma textura por label, e reaproveita cada um deles enquanto ainda representa a mesma coisa — um mundo em movimento é uma posição escrita num mesh que já está lá.

Vale conhecer dois limites.

imprime
hive: a window draws one scene, and this view has 2 — the first one is the one on screen

Uma janela desenha uma cena. Uma view com duas desenha a primeira e diz isso na saída padrão, uma vez, em vez de deixar uma segunda imagem silenciosamente em branco.

Uma cena renderizada por ui.html ou ui.page é uma caixa vazia. Uma página servida não tem socket, então não tem quadros para desenhar — exatamente como um botão numa página dessas não tem para onde mandar a mensagem dele. O HTML em volta é real; o mundo dentro não é.

O three.js é baixado em tempo de compilação, e só para uma cena

Uma cena é desenhada com three.js, e essa é a única coisa que o compilador baixa para o runtime dele — os drivers de banco do passo 112, e o que um import Go ou remoto trouxer consigo, são dependências do seu próprio programa, resolvidas pela toolchain do Go. A biblioteca é fixada numa versão e no SHA-256 de cada um dos arquivos dela: um download que der um hash diferente falha o build, em vez de ser usado ou cacheado — o que cobre um download corrompido, um adulterado, e um registro que republicou uma versão em silêncio. Ela é baixada uma vez, para ~/.hive/vendor, embutida no executável, e servida pela própria janela.

Então o programa que você distribui continua sendo um arquivo único que roda numa máquina sem rede, e um programa que não desenha cena nenhuma não baixa nem carrega isso. Só o primeiro build de uma cena precisa de rede para isso.

164A world with no windowUm mundo sem janela

This is the payoff, and the reason a scene is a value rather than a canvas. A world is a func returning a vector, so a test reads it the way it reads any other vector — no window, no screenshot, no second machine:

// A scene is a value too, so what is *in* the world is as readable as the panel
// around it — no picture required.
test "the world holds the arena, and a player is four shapes of it" {
	empty := len(fps.world(still()))
	crowded := len(fps.world(joined(still(), [standing("127.0.0.1:9201", 2.0, 2.0)])))
	assert crowded == empty + 4

	// Somebody who has gone down is one shape lying on the floor, not four.
	mut fps.Foe fallen = standing("127.0.0.1:9201", 2.0, 2.0)
	fallen.health = 0
	assert len(fps.world(joined(still(), [fallen]))) == empty + 1
}

And because the rules of the world are funcs of the state too, so is everything a game is actually about. Walking, falling, cover, whether a shot connected — none of it needs a mouse:

test "walking forward goes the way you are looking" {
	from := still().body
	after := walking(holding(still(), ["w"]), 1.0)
	// A second at 6.4 metres a second, all of it towards -Z.
	assert after.z < from.z - 6.0
	assert after.z > from.z - 7.0
	assert math.abs(after.x - from.x) < 0.001
}

test "holding two keys does not make you faster" {
	from := still().body
	straight := walking(holding(still(), ["w"]), 1.0)
	diagonal := walking(holding(still(), ["w", "d"]), 1.0)
	far := math.hypot(straight.x - from.x, straight.z - from.z)
	slant := math.hypot(diagonal.x - from.x, diagonal.z - from.z)
	assert math.abs(far - slant) < 0.01
}

The panel around the picture is read with ui.html, exactly as step 158 read a form — the same renderer, and still no browser. So a first-person shooter has an ordinary test suite: it walks a player into a wall, shoots one through a crate, and has a zombie bite somebody, with nothing on screen at all.

The whole thing: code-examples/19 - EXAMPLE APP - Multiplayer FPS/ — a shooter for one player against the dead, or for several over a peer-to-peer mesh, where the arena, the collisions, the bullets and the monsters are all funcs of the state. Its suite, fps.test.hive, is where the tests above come from.

Esta é a recompensa, e o motivo de uma cena ser um valor e não um canvas. Um mundo é um func que devolve um vetor, então um teste o lê como leria qualquer outro vetor — sem janela, sem captura de tela, sem segunda máquina:

// Uma cena também é um valor, então o que está *dentro* do mundo é tão legível
// quanto o painel em volta — sem precisar de imagem.
test "the world holds the arena, and a player is four shapes of it" {
	empty := len(fps.world(still()))
	crowded := len(fps.world(joined(still(), [standing("127.0.0.1:9201", 2.0, 2.0)])))
	assert crowded == empty + 4

	// Quem caiu é uma forma deitada no chão, não quatro.
	mut fps.Foe fallen = standing("127.0.0.1:9201", 2.0, 2.0)
	fallen.health = 0
	assert len(fps.world(joined(still(), [fallen]))) == empty + 1
}

E como as regras do mundo também são funcs do estado, tudo aquilo de que um jogo realmente trata também é. Andar, cair, cobertura, se um tiro pegou — nada disso precisa de mouse:

test "walking forward goes the way you are looking" {
	from := still().body
	after := walking(holding(still(), ["w"]), 1.0)
	// Um segundo a 6,4 metros por segundo, tudo na direção -Z.
	assert after.z < from.z - 6.0
	assert after.z > from.z - 7.0
	assert math.abs(after.x - from.x) < 0.001
}

test "holding two keys does not make you faster" {
	from := still().body
	straight := walking(holding(still(), ["w"]), 1.0)
	diagonal := walking(holding(still(), ["w", "d"]), 1.0)
	far := math.hypot(straight.x - from.x, straight.z - from.z)
	slant := math.hypot(diagonal.x - from.x, diagonal.z - from.z)
	assert math.abs(far - slant) < 0.01
}

O painel em volta da imagem é lido com ui.html, exatamente como o passo 158 leu um formulário — o mesmo renderizador, e ainda sem navegador. Então um jogo em primeira pessoa tem uma suíte de testes comum: ela caminha um jogador contra uma parede, atira em alguém através de um engradado, e faz um zumbi morder outro, com absolutamente nada na tela.

A coisa inteira: code-examples/19 - EXAMPLE APP - Multiplayer FPS/ — um jogo de tiro para uma pessoa contra os mortos, ou para várias por uma malha ponto a ponto, onde a arena, as colisões, as balas e os monstros são todos funcs do estado. A suíte dele, fps.test.hive, é de onde vêm os testes acima.
Part XVIIIParte XVIII

TestingTestes

A declaration, a keyword you already have, and a number nobody had to ask for.Uma declaração, uma palavra que você já tem, e um número que ninguém precisou pedir.

165A test is a declarationUm teste é uma declaração

A test sits at the top level, beside proc, func, query and type. It is named in prose, because a test name is documentation rather than something anything calls.

func total(prices: Int[]): Int {
	mut sum := 0
	for each p in prices {
		sum = sum + p
	}
	return sum
}

test "an empty basket costs nothing" {
	Int[0] empty = []
	assert total(empty) == 0
}

test "prices add up" {
	Int[2] two = [3, 4]
	assert total(two) == 7
}
prints
hive test basket.hive

  PASS  an empty basket costs nothing
  PASS  prices add up

  2 tests: 2 passed
  coverage: 100.0% of statements (4/4)

For the same reason a test takes no parameters and returns nothing: it is run, never called, so there is no caller to give it either. Its body is an ordinary proc body — it may call procs, read files, reach the standard library.

Keep in mind

There is nothing to install and nothing to register. No framework, no third-party library, no describe/it, and no assertion vocabulary beyond the assert you met in step 53.

Um teste fica no nível de cima, ao lado de proc, func, query e type. Ele é nomeado em prosa, porque o nome de um teste é documentação, não algo que alguém chama.

func total(prices: Int[]): Int {
	mut sum := 0
	for each p in prices {
		sum = sum + p
	}
	return sum
}

test "an empty basket costs nothing" {
	Int[0] empty = []
	assert total(empty) == 0
}

test "prices add up" {
	Int[2] two = [3, 4]
	assert total(two) == 7
}
imprime
hive test basket.hive

  PASS  an empty basket costs nothing
  PASS  prices add up

  2 tests: 2 passed
  coverage: 100.0% of statements (4/4)

Pelo mesmo motivo um teste não recebe parâmetros e não devolve nada: ele é rodado, nunca chamado, então também não há quem chame para lhe dar algo. O corpo é um corpo de proc comum — pode chamar procs, ler arquivos, usar a biblioteca padrão.

Guarde isto

Não há nada para instalar e nada para registrar. Sem framework, sem biblioteca de terceiros, sem describe/it, e sem vocabulário de asserção além do assert que você viu no passo 53.

166assert records instead of stoppingassert registra em vez de parar

assert is the keyword you already have, and it says what it always said: this must hold. What changes is the consequence — and, as everywhere else in Hive, that is decided by where it is written.

proc main(): void {
	assert 1 == 2      // the program is wrong: it stops here
}

test "a failing check" {
	assert 1 == 2      // the test is wrong: it is recorded, and the suite goes on
}

Outside a test, a failed assertion has proved the program wrong, so the program stops. Inside one it has proved the test wrong, so the failure is recorded and every test after it still runs. A test that panics fails on its own too, rather than taking the rest of the suite with it — one broken test cannot hide every result behind it.

Keep in mind

This is the same shape as f(x) against async f(x): one spelling, and the position it is written in decides what it means.

assert é a palavra que você já tem, e ela diz o que sempre disse: isto tem que valer. O que muda é a consequência — e, como em todo o resto do Hive, isso é decidido por onde ela está escrita.

proc main(): void {
	assert 1 == 2      // o programa está errado: ele para aqui
}

test "a failing check" {
	assert 1 == 2      // o teste está errado: fica registrado, e a suíte segue
}

Fora de um teste, uma asserção que falha provou que o programa está errado, então o programa para. Dentro de um, ela provou que o teste está errado, então a falha é registrada e todo teste depois dele ainda roda. Um teste que dá panic também falha sozinho, em vez de levar o resto da suíte junto — um teste quebrado não pode esconder todos os resultados atrás dele.

Guarde isto

É o mesmo formato de f(x) contra async f(x): uma grafia só, e a posição em que está escrita decide o que ela significa.

167A failure shows both sidesUma falha mostra os dois lados

The compiler has the source text of the condition and the static types of its parts, so a failed comparison does not have to be explained by hand.

test "prices add up" {
	Int[2] two = [3, 4]
	assert total(two) == 8
}
prints
  FAIL  prices add up
        basket.hive:12: assert total(two) == 8
          left:  7
          right: 8

  1 test: 0 passed, 1 failed

The position is your file and line, not the Go the compiler generated underneath. So is the condition: the name you wrote is the name you are shown, even where a bare builtin or an imported name was rewritten on the way down.

Keep in mind

hive test exits non-zero when anything failed, which is what makes it the thing a commit hook or a CI step runs.

O compilador tem o texto-fonte da condição e os tipos estáticos das partes dela, então uma comparação que falha não precisa ser explicada à mão.

test "prices add up" {
	Int[2] two = [3, 4]
	assert total(two) == 8
}
imprime
  FAIL  prices add up
        basket.hive:12: assert total(two) == 8
          left:  7
          right: 8

  1 test: 0 passed, 1 failed

A posição é o seu arquivo e a sua linha, não o Go que o compilador gerou por baixo. A condição também: o nome que você escreveu é o nome que você vê, mesmo onde um builtin sem qualificação ou um nome importado foi reescrito no caminho.

Guarde isto

O hive test sai com código diferente de zero quando algo falhou, que é o que faz dele a coisa que um hook de commit ou um passo de CI roda.

168Coverage is not a separate commandCobertura não é um comando à parte

Every run reports it. A test run that does not say what it missed has answered half the question, so there is no flag to remember and no second command to forget.

prints
  6 tests: 6 passed
  coverage: 87.5% of statements (7/8)
  never exercised: describe

The percentage counts statements of your own declarations — the clone helpers, ordering helpers, atom table and JSON machinery the compiler writes alongside them are nobody's code, and not something a test can be said to have missed. never exercised names the declarations no test reached at all, which is usually the line worth acting on. With more than one file in the program, a per-file breakdown is printed too.

Tests may live beside the code they are about or in a file of their own. A file holding only tests needs no main, and hive test on a program runs every test in every file the entrypoint reaches — so import is also how a suite is assembled.

Keep in mind

The runner is the Go toolchain the compiler already drives, and the tests compile into the same Go package as the program. That is what lets a test reach every proc, func and type you declared without any of them being exported, or renamed, or made testable on purpose.

Toda execução reporta. Uma rodada de testes que não diz o que deixou de fora respondeu metade da pergunta, então não há flag para lembrar nem segundo comando para esquecer.

imprime
  6 tests: 6 passed
  coverage: 87.5% of statements (7/8)
  never exercised: describe

A porcentagem conta instruções das suas próprias declarações — os helpers de cópia, os de ordenação, a tabela de atoms e o maquinário de JSON que o compilador escreve ao lado não são código de ninguém, e não são algo que um teste possa ter deixado de fora. O never exercised nomeia as declarações que nenhum teste alcançou, que costuma ser a linha que vale agir. Com mais de um arquivo no programa, uma quebra por arquivo também é impressa.

Os testes podem morar ao lado do código de que tratam ou em um arquivo próprio. Um arquivo só com testes não precisa de main, e o hive test em um programa roda todo teste de todo arquivo que o ponto de entrada alcança — então o import também é como se monta uma suíte.

Guarde isto

Quem roda é a ferramenta Go que o compilador já usa, e os testes compilam no mesmo pacote Go do programa. É isso que permite a um teste alcançar toda proc, func e type que você declarou sem que nenhuma delas seja exportada, renomeada, ou tornada testável de propósito.

Part XIXParte XIX

Under the hoodPor baixo do capô

What happens between your source file and a running binary.O que acontece entre o seu arquivo-fonte e um binário rodando.

169The passes your program goes throughAs passagens pelas quais seu programa passa

The compiler is a pipeline, and every stage in it has already come up in this tour.

O compilador é um pipeline, e cada etapa dele já apareceu neste tour.

.hiveyour sourceseu código
lexertokens: strings, atoms, SQL bodiestokens: strings, átomos, corpos SQL
parserthe syntax treea árvore sintática
modulesstep 143 — resolves imports, rejects cyclespasso 143 — resolve imports, recusa ciclos
genericsstep 80 — one copy per instantiationpasso 80 — uma cópia por instanciação
boundsstep 31 — proves every index in rangepasso 31 — prova todo índice na faixa
codegenpurity checks, value semantics, outputpureza, semântica de valor, saída
native binarybinário nativofor this machinepara esta máquina

Two of those stages are unusual enough to be worth naming again. generics rewrites every generic callable into concrete copies, so every later stage only ever sees ordinary code. And bounds is a flow-sensitive pass whose only job is proving indexes safe — which is why "the compiler can see that guard" was a real statement about a real pass, and not a figure of speech.

src/hive/lexer.gleam      source text -> tokens
src/hive/parser.gleam     tokens      -> syntax tree
src/hive/modules.gleam    resolves the import graph, flattens the program
src/hive/generics.gleam   monomorphization
src/hive/bounds.gleam     vector index and slice bounds checking
src/hive/codegen.gleam    syntax tree -> output, with local type inference
src/hive/runtime.gleam    the runtime, plus one source per hive.* module used
src/hive/compiler.gleam   glue, and the func/proc purity checks
src/hive/cli.gleam        drives the toolchain

The compiler itself is written in Gleam. Run its tests with gleam test — they include compiling every shipped example, which is what keeps the examples honest.

Duas dessas etapas são incomuns o suficiente para valer nomear de novo. O generics reescreve todo callable genérico em cópias concretas, então toda etapa posterior só vê código comum. E o bounds é uma passagem sensível a fluxo cujo único trabalho é provar que índices são seguros — que é por que "o compilador consegue ver essa guarda" era uma afirmação real sobre uma passagem real, e não figura de linguagem.

src/hive/lexer.gleam      texto-fonte -> tokens
src/hive/parser.gleam     tokens      -> árvore sintática
src/hive/modules.gleam    resolve o grafo de imports, achata o programa
src/hive/generics.gleam   monomorfização
src/hive/bounds.gleam     checagem de limites de índice e slice
src/hive/codegen.gleam    árvore -> saída, com inferência local de tipos
src/hive/runtime.gleam    o runtime, mais um fonte por módulo hive.* usado
src/hive/compiler.gleam   cola, e as checagens de pureza de func/proc
src/hive/cli.gleam        conduz a toolchain

O compilador em si é escrito em Gleam. Rode os testes dele com gleam test — eles incluem compilar todo exemplo que acompanha o projeto, que é o que mantém os exemplos honestos.

170What it compiles toPara o que ele compila

Hive lowers to Go, and then the Go toolchain produces the executable. You never write or read that Go — but a handful of Hive's rules make more sense once you know what they become, so here are the ones worth knowing.

Hivebecomes
proc / func / queryan ordinary function
type T { } / type T { A B }a struct / an interface plus one struct per variant
Str, Int, Float, Boolstring, int, float64, bool
Str[3], Str[dyn], Str[]all three become a slice — which is why Part V exists
mutnothing — it is compile-time only
mut b = a (both mut)no variable at all: b compiles to a, one header for both
async f(x)go f(x) — the virtual thread of Part XI
x := async f(a)one goroutine, and every read of x is a join on it
await [f(a), f(b)]one goroutine each, then a blocking join on both
ys := xs needing a copya generated clone, chosen by the type (step 45)
func f(v: T[]) at T = Strf_Str — one copy per instantiation
f(a, _, c)a closure whose parameter is the hole
#Atoma small integer constant, plus the embedded atom table
t[1:3]t[1:4] — Hive's high bound is inclusive (step 30)
query bodytext with placeholders, plus the bound arguments

A few consequences fall out of that list. Codegen runs a light type-inference pass over locals so overloaded syntax picks the right lowering — + on vectors versus strings versus numbers, atom-to-Str coercion, zero-safe division. That same pass is what identifies the constructs with no honest lowering — indexing a Str is the one that exists today — and rejects them as Hive errors rather than emitting output that either fails to compile or quietly means something else.

And an identifier of yours that happens to be a keyword in the target language but not in Hive — a variable named range or select — is renamed consistently at its definition and every use, so it never collides with a grammar you did not write in.

O Hive baixa para Go, e então a toolchain do Go produz o executável. Você nunca escreve nem lê esse Go — mas algumas regras do Hive fazem mais sentido quando você sabe no que elas se transformam, então aqui estão as que valem a pena.

Hivese torna
proc / func / queryuma função comum
type T { } / type T { A B }um struct / uma interface mais um struct por variante
Str, Int, Float, Boolstring, int, float64, bool
Str[3], Str[dyn], Str[]os três se tornam um slice — que é por que a Parte V existe
mutnada — existe só em tempo de compilação
mut b = a (os dois mut)nenhuma variável: b compila para a, um header para os dois
async f(x)go f(x) — a thread virtual da Parte XI
x := async f(a)uma goroutine, e cada leitura de x é um join nela
await [f(a), f(b)]uma goroutine para cada, e uma junção bloqueante nas duas
ys := xs que precisa de cópiaum clone gerado, escolhido pelo tipo (passo 45)
func f(v: T[]) em T = Strf_Str — uma cópia por instanciação
f(a, _, c)uma closure cujo parâmetro é o buraco
#Atomuma constante inteira pequena, mais a tabela de átomos embutida
t[1:3]t[1:4] — o limite superior do Hive é inclusivo (passo 30)
corpo de querytexto com placeholders, mais os argumentos vinculados

Algumas consequências saem dessa lista. O codegen roda uma inferência leve de tipos sobre os locais para que a sintaxe sobrecarregada escolha a tradução certa — + em vetores versus strings versus números, coerção de átomo para Str, divisão segura por zero. Essa mesma passagem é o que identifica as construções sem tradução honesta — indexar um Str é a que existe hoje — e as rejeita como erros do Hive, em vez de emitir saída que ou não compila ou silenciosamente significa outra coisa.

E um identificador seu que por acaso é palavra-chave na linguagem de destino mas não no Hive — uma variável chamada range ou select — é renomeado consistentemente na definição e em todo uso, para nunca colidir com uma gramática na qual você não escreveu.

171Where the edges areOnde estão as bordas

The tour is over; here is what it did not show you, because it does not exist yet.

The compiler currently targets exactly the constructs that appear in code-examples/. The lexer, parser and code generator are written to be extended, but there is no full type checker and no standard library beyond the hive.* modules in Parts XIII and XIV.

The one place the compiler goes further than that is vector bounds: a dedicated flow-sensitive pass proves every index and slice in range, which is what steps 31 to 35 were about, and why a Hive program cannot fail at runtime with an out-of-range index.

Also not built yet, from the distribution module: compile-time rejection of a wrong-typed message at the call site, message fragmentation, a WebSocket transport, pinned per-node keys, and supervision trees.

Where to go next

Every example in code-examples/ compiles, builds and runs, and together they double as the language's specification. Read them in order — they are numbered — and start with 1 - Basic IO. If you want the reference rather than the tour, the repository's README.md is organised by feature rather than by lesson.

O tour terminou; aqui está o que ele não mostrou, porque ainda não existe.

O compilador hoje cobre exatamente as construções que aparecem em code-examples/. O lexer, o parser e o gerador de código foram escritos para serem estendidos, mas não existe um verificador de tipos completo nem biblioteca padrão além dos módulos hive.* das Partes XIII e XIV.

O único ponto em que o compilador vai além disso é o de limites de vetor: uma passagem dedicada e sensível a fluxo prova que todo índice e slice está na faixa, que é do que tratavam os passos 31 a 35, e por que um programa Hive não pode falhar em execução com índice fora da faixa.

Também ainda não construído, do módulo de distribuição: recusa em tempo de compilação de mensagem com tipo errado no ponto de chamada, fragmentação de mensagem, um transporte WebSocket, chaves fixadas por nó, e árvores de supervisão.

Para onde ir agora

Todo exemplo em code-examples/ compila, builda e roda, e juntos eles servem também como especificação da linguagem. Leia-os em ordem — eles são numerados — e comece pelo 1 - Basic IO. Se você quer a referência em vez do tour, o README.md do repositório é organizado por recurso, não por lição.