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.
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 curioushive 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 curiosidadeO 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."
}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"]
}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."
}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"]
}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.
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.
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.
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}."
}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}."
}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
}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
}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.
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.
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
}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
}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.
| expression | result |
|---|---|
a / 0, a % 0 | 0 — dividing by zero is a value, not a crash |
Int overflow | wraps, silently, two's-complement |
2 ** 100 | 0 — that same wrap, reached by multiplying |
n ** k, k < 0 | 0 — including 1 ** -1 |
n ** 0 | 1 |
-7 % 3 | -1 — the sign follows the dividend |
10.0 ** 400.0 | +Inf — Float 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.
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ão | resultado |
|---|---|
a / 0, a % 0 | 0 — dividir por zero é um valor, não um crash |
estouro de Int | dá a volta, silenciosamente, em complemento de dois |
2 ** 100 | 0 — a mesma volta, alcançada multiplicando |
n ** k, k < 0 | 0 — incluindo 1 ** -1 |
n ** 0 | 1 |
-7 % 3 | -1 — o sinal segue o dividendo |
10.0 ** 400.0 | +Inf — Float 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.
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
}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:
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
}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:
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).
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.
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.
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
}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
}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]
}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.
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]
}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.
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
}[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")
}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.
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
}[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")
}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.
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.
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
}[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
}[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 literalA 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.
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 literalUm 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.
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
}[Beeswax Smoker fuel Hive tool]
It is a statement, not an expression — it returns nothing, and there is no new vector to catch.
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.
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.
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
}[Beeswax Smoker fuel Hive tool]
É um comando, não uma expressão — não retorna nada, e não há vetor novo para pegar.
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.
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.
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)
}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)
}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 rowHive'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 linhaO 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 3On 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
}
}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 3Num 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
}
}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 knownThe 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 é conhecidoA ú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 resizemut 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 redimensionamut 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] takerYou 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, " | ")
}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, " | ")
}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"
}
}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"
}
}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"), ", ")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"), ", ")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.
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 itThe 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 issoA 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 ⟵ source | decision |
|---|---|
| immutable ⟵ immutable | share — neither side can ever mutate it |
mut ⟵ mut | share — shared mutable state is the intent |
mut ⟵ immutable | share if the target is never written through, else copy |
immutable ⟵ mut | share 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 ⟵ origem | decisão |
|---|---|
| imutável ⟵ imutável | compartilha — nenhum dos lados pode mutar |
mut ⟵ mut | compartilha — estado mutável compartilhado é a intenção |
mut ⟵ imutável | compartilha se o destino nunca for escrito, senão copia |
imutável ⟵ mut | compartilha 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
}[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
}[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
}[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 1Two 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
}[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) é 1Dois 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
}[[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
}[[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.
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.
code-examples/6 - Value Semantics/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"
}
}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"
}
}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}"
}
}pass 0 pass 1 pass 2
The counter is scoped to the loop and implicitly mutable, so it needs no
mut — i++ 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}"
}
}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
}
}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
}
}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
}
}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
}
}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.
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.
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.
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")
}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")
}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
mutvalue 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
mutpassado 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 oneWhich 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)
}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 impuroO 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)
}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)
}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)
}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
funcvalue may be used where aprocis expected. Pure widens to impure — the slot promised less than it got. - A
procvalue may not fill afuncslot. - A
funcstill 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
funcpode ser usado onde se espera umproc. Puro se alarga para impuro — o slot prometeu menos do que recebeu. - Um valor
procnão pode preencher um slotfunc. - Uma
funcainda 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.
| builtin | signature | gives you |
|---|---|---|
map | map(T[], func(T): K): K[dyn] | every element, transformed |
filter | filter(T[], func(T): Bool): T[dyn] | the ones that passed |
filterMap | filterMap(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), " ")
}#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 threeThe 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 onceTrê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.
| builtin | assinatura | devolve |
|---|---|---|
map | map(T[], func(T): K): K[dyn] | todo elemento, transformado |
filter | filter(T[], func(T): Bool): T[dyn] | os que passaram |
filterMap | filterMap(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), " ")
}#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êsA 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 vez64Putting 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
}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|figThe 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
}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|figA 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` untouchedBoth 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 placeBoth 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` intocadoAs 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 lugarOs 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 neededConcatenation 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 nothingUm 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 guardaA 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 nada67The 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.
| function | what 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 / filterMap | walk 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ção | o 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 / filterMap | caminha 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.
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.
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.
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
}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
}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.
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"
}
}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"
}
}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
}
}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
}
}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"
}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"
}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"
}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}"caughtimg/logo.pngwhole.
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"
}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}"pegouimg/logo.pnginteiro.
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.
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.
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ó.
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.
code-examples/7 - Pattern Matching/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 Intada 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 Intada 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.
func first(v: T[]): Result<T, Bool>first_Strfirst(names) — Str[2]first_Intfirst(counts) — Int[dyn]first_Boolnever called, never emittedBecause 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.
func first(v: T[]): Result<T, Bool>first_Strfirst(names) — Str[2]first_Intfirst(counts) — Int[dyn]first_Boolnunca chamado, nunca geradoComo 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=BoolNote 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=BoolRepare 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)
}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)
}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.
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.
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.
code-examples/14 - Generics/Duas recusas, e as duas seguem do passo 80 em vez de serem regras extras.
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.
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.
code-examples/14 - Generics/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")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")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"
}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.
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"
}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.
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
}[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.
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
}[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.
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
}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
}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
}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
}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"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"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.
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 itSo 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.
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 eleEntã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.
code-examples/10 - Concurrency/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 runtimeas csv is optional — a bare path is a CSV — and separating by
overrides the comma with any string, not just a single character.
| form | yields |
|---|---|
using <path> / as csv | Result<Table, hive.TableError> |
as xlsx / as ods | Result<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çãoas csv é opcional — um caminho puro é um CSV — e separating by troca
a vírgula por qualquer string, não só um caractere.
| forma | entrega |
|---|---|
using <caminho> / as csv | Result<Table, hive.TableError> |
as xlsx / as ods | Result<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.
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}"
}
}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}"
}
}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:
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:
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}"
}
}wrote 13 bytes the file holds 1 lines reading ./scratch/nothing-here.txt failed with NotFound
| call | what it does |
|---|---|
read / lines | the whole file / split on newlines |
write / append | replace / add to the end — both report the bytes written |
exists / size | a Bool (a directory counts) / bytes |
delete | a file, or an already-empty directory |
list / makeDir | entry names, sorted / create with parents |
copy / move | copy 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.
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}"
}
}wrote 13 bytes the file holds 1 lines reading ./scratch/nothing-here.txt failed with NotFound
| chamada | o que faz |
|---|---|
read / lines | o arquivo inteiro / quebrado por linhas |
write / append | substitui / adiciona ao fim — ambos reportam os bytes escritos |
exists / size | um Bool (diretório conta) / bytes |
delete | um arquivo, ou um diretório já vazio |
list / makeDir | nomes das entradas, ordenados / cria com os pais |
copy / move | copia 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.
hive.file, rodando:
code-examples/12 - Files and Spreadsheets/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 case — SELECT, 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úsculas — SELECT, 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}
}| declared | using conn run q(…) yields |
|---|---|
Row[dyn] (a declared type) | Result<Row[dyn], hive.sql.SqlError> |
Str[dyn], Int[dyn], … | that column, as a vector |
void | Result<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}
}| declarado | using conn run q(…) entrega |
|---|---|
Row[dyn] (um tipo declarado) | Result<Row[dyn], hive.sql.SqlError> |
Str[dyn], Int[dyn], … | aquela coluna, como vetor |
void | Result<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 dataSo 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ó dadoAssim, 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
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
voidstatement's select list, as inINSERT INTO a SELECT * FROM b— that is not a result at all.
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 emINSERT 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
WHEREclause — so there is noWHERE 1 = 1to 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 = 1para 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
}[[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
}[[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.
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.
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 string | one query at a time | eight at once |
|---|---|---|
:memory:, default pool | works | fails intermittently: no such table |
:memory:, pool(…, 1, 1) | works | works, one at a time |
file::memory:?cache=shared | works | works |
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ão | uma query por vez | oito ao mesmo tempo |
|---|---|---|
:memory:, pool padrão | funciona | falha às vezes: no such table |
:memory:, pool(…, 1, 1) | funciona | funciona, uma por vez |
file::memory:?cache=shared | funciona | funciona |
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:
reason | what 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.
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:
reason | o 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.
code-examples/5 - SQL/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}"
}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}"
}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 for | the calls |
|---|---|
| constants | pi() |
| angles | sin(x) cos(x) tan(x) asin(x) acos(x) atan2(y, x) |
| distances | sqrt(x) hypot(x, y) abs(x) |
| bounds | min(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.
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.
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 |
|---|---|
| constantes | pi() |
| ângulos | sin(x) cos(x) tan(x) asin(x) acos(x) atan2(y, x) |
| distâncias | sqrt(x) hypot(x, y) abs(x) |
| limites | min(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.
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.
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")){"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")){"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 charsHashing 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 hexHash 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.
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.
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.
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.
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}"
}
}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.
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}"
}
}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.
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")
}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")
}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.
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.
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.
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.
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.
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.
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 120Tuesday, 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 é 120Tuesday, 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 keyStorage 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]]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.
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 chaveArmazenamento 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]]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.
code-examples/18 - Maps/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 | |
|---|---|---|
| lifetime | as long as the call takes | unscoped — outlives everything |
| identity | none, there is nothing to hold | yes, that is the point |
| interaction | none — it keeps nothing back | called with a message |
| as a value | it has none at all | ordinary value; can be sent inside a message |
| result | none, unless the call was given a name | the 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 vida | o tempo que a chamada levar | sem escopo — sobrevive a tudo |
| identidade | nenhuma, não há o que segurar | sim, é justamente o ponto |
| interação | nenhuma — não guarda nada | chamado com uma mensagem |
| como valor | ela não tem valor nenhum | valor comum; pode ir dentro de uma mensagem |
| resultado | nenhum, a não ser que a chamada tenha recebido um nome | a 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): Statetype 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): Statetype 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 deadlineExiste 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 prazomain 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ó 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.
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.
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.
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.
code-examples/13 - Distributed Actors/ ·
Um cache de três nós combinando serviços com SQL:
code-examples/9 - EXAMPLE APP - Online Cache/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/text → text), 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/text → text), 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:
hive: this import forms a cycle:
lib/inventory.hive
-> lib/pricing.hive
-> lib/inventory.hivecode-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:
hive: this import forms a cycle:
lib/inventory.hive
-> lib/pricing.hive
-> lib/inventory.hivecode-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 spaceThe module is named after the file inside the repository, not after the repository or its host
(.../src/text → text) — 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 3b4ae8e176fc35fb3d74076156e444e835522714So 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.
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.
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çoO módulo recebe o nome do arquivo dentro do repositório, não do repositório nem do host
(.../src/text → text) — 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 3b4ae8e176fc35fb3d74076156e444e835522714Entã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.
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 é.
code-examples/11 - Modules/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 backHive 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 itAttributes 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 colourA 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.
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 devolveHive 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 eleAtributos 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 corUm 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ê.
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.
| widget | payload | is |
|---|---|---|
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) | View | drawn above everything, with a backdrop |
text(attrs, content) | Str | the one true leaf |
image(attrs, src, alt) | Str, Str | a picture, described |
icon(attrs, name) | Icon | one of a closed set of eighteen |
link(attrs, href, label) | Str, Str | a destination, which needs no event to work |
button(attrs, label) | Str | |
input(attrs, value) | Str | one line; kind says which sort |
textarea(attrs, value) | Str | several lines |
checkbox(attrs, label, checked) | Str, Bool | label included, and clickable |
select(attrs, options, chosen) | Str[], Str | one of a few |
table(attrs, rows) | Table | the 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.
| widget | carga | é |
|---|---|---|
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) | View | desenhado acima de tudo, com um fundo |
text(attrs, content) | Str | a única folha de verdade |
image(attrs, src, alt) | Str, Str | uma imagem, descrita |
icon(attrs, name) | Icon | um de um conjunto fechado de dezoito |
link(attrs, href, label) | Str, Str | um destino, que funciona sem evento nenhum |
button(attrs, label) | Str | |
input(attrs, value) | Str | uma linha; o kind diz de que tipo |
textarea(attrs, value) | Str | várias linhas |
checkbox(attrs, label, checked) | Str, Bool | com o rótulo incluído, e clicável |
select(attrs, options, chosen) | Str[], Str | um entre alguns |
table(attrs, rows) | Table | a 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)| attribute | carries |
|---|---|
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")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)| atributo | carrega |
|---|---|
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")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().
| type | variants |
|---|---|
Align | Start Center End Stretch |
Justify | Start Center End Between |
TextSize | Title Subtitle Body Caption |
Tone | Normal Muted Good Warn Danger HEX(Str) RGBA(Int, Int, Int, Int) |
Axis | Horizontal Vertical Both |
InputKind | Text Number Date Password Search |
Icon | Search 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().
| tipo | variantes |
|---|---|
Align | Start Center End Stretch |
Justify | Start Center End Between |
TextSize | Title Subtitle Body Caption |
Tone | Normal Muted Good Warn Danger HEX(Str) RGBA(Int, Int, Int, Int) |
Axis | Horizontal Vertical Both |
InputKind | Text Number Date Password Search |
Icon | Search 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.
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 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.
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.
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.
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.
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.
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.
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.
| shape | payload | is |
|---|---|---|
box(attrs, width, height, depth) | three Floats | |
sphere(attrs, radius) | Float | |
cylinder(attrs, radius, height) | Float, Float | |
ground(attrs, width, depth) | Float, Float | a plane that lies flat without being turned |
label(attrs, words) | Str | words in the world, always facing whoever is looking |
line(attrs, fromX, fromY, fromZ, toX, toY, toZ) | six Floats | the 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.
| forma | carga | é |
|---|---|---|
box(attrs, width, height, depth) | três Floats | |
sphere(attrs, radius) | Float | |
cylinder(attrs, radius, height) | Float, Float | |
ground(attrs, width, depth) | Float, Float | um plano que fica deitado sem precisar ser virado |
label(attrs, words) | Str | palavras no mundo, sempre viradas para quem olha |
line(attrs, fromX, fromY, fromZ, toX, toY, toZ) | seis Floats | a ú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.
| attribute | is |
|---|---|
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.
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
}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
}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): MsgonFrame 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.
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): MsgO 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.
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.
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.
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.
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 é.
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.
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.
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.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
}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.
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
}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.
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.
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.
É 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
} FAIL prices add up
basket.hive:12: assert total(two) == 8
left: 7
right: 8
1 test: 0 passed, 1 failedThe 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.
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
} FAIL prices add up
basket.hive:12: assert total(two) == 8
left: 7
right: 8
1 test: 0 passed, 1 failedA 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.
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.
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.
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.
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.
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.
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ódigoTwo 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 toolchainThe 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 toolchainO 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.
| Hive | becomes |
|---|---|
proc / func / query | an ordinary function |
type T { } / type T { A B } | a struct / an interface plus one struct per variant |
Str, Int, Float, Bool | string, int, float64, bool |
Str[3], Str[dyn], Str[] | all three become a slice — which is why Part V exists |
mut | nothing — 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 copy | a generated clone, chosen by the type (step 45) |
func f(v: T[]) at T = Str | f_Str — one copy per instantiation |
f(a, _, c) | a closure whose parameter is the hole |
#Atom | a small integer constant, plus the embedded atom table |
t[1:3] | t[1:4] — Hive's high bound is inclusive (step 30) |
query body | text 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.
| Hive | se torna |
|---|---|
proc / func / query | uma função comum |
type T { } / type T { A B } | um struct / uma interface mais um struct por variante |
Str, Int, Float, Bool | string, int, float64, bool |
Str[3], Str[dyn], Str[] | os três se tornam um slice — que é por que a Parte V existe |
mut | nada — 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ópia | um clone gerado, escolhido pelo tipo (passo 45) |
func f(v: T[]) em T = Str | f_Str — uma cópia por instanciação |
f(a, _, c) | uma closure cujo parâmetro é o buraco |
#Atom | uma 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 query | texto 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.
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.
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.