Um tour guiado e aprofundado pela linguagem. Todo trecho é executável - cole no REPL ou salve como um arquivo .brn. Onde aparecerem símbolos, use o botão Show written form para alternar o mesmo código para o seu equivalente em palavras.
-- Output: ...). Para uma referência concisa de cada recurso e da biblioteca padrão completa, veja a Documentação. Para uma amostra rápida, os exemplos da página inicial são uma versão condensada desta página.
Bern é uma linguagem pequena, de inspiração funcional, construída em torno de uma única ideia: o código deve ser lido da forma como você pensa sobre o problema. Três hábitos farão o resto desta página fazer sentido:
print para experimentos rápidos.if, case e corpos de função produzem um valor, então podem ser atribuídos, retornados ou encaminhados por pipe.a + b e a plus b são o mesmo programa. Iniciantes podem escrever palavras; quando os símbolos ficarem naturais, troque à vontade.Escreva um valor em sua própria linha e Bern o avalia e imprime. Não há função main para configurar e nada para importar no básico.
"Hello, World!" 23 3.14 true
Cada uma dessas quatro linhas produz saída em ordem. Strings aceitam as sequências de escape habituais, e você pode construir novas strings com +:
"Bern" + " " + "rocks" -- Output: "Bern rocks" "Line one\nLine two" -- Output: duas linhas, porque \n é uma quebra de linha
'n') é um caractere, enquanto aspas duplas ("n") criam uma string de um caractere. Indexar uma string retorna caracteres.
Vincule um nome a um valor com name = value. Bern infere o tipo; você nunca o anota. Os quatro tipos escalares centrais são inteiros, doubles, strings e booleanos.
integer = 2 double = 3.14 helloworld = "Hello, World!" isActive = true
Ler uma variável é apenas escrever o seu nome. Como avaliação implica saída, o valor é impresso:
helloworld -- Output: "Hello, World!" integer + 40 -- Output: 42
Dois operadores prefixos permitem inspecionar valores:
:: (também escrito typeof) retorna o tipo de um valor como uma string.:> (também escrito length) retorna o comprimento de uma lista, conjunto ou string.:: 2 -- Output: "Int" :: "hello" -- Output: "String" :> [1, 2, 3, 4] -- Output: 4 :> "hello" -- Output: 5
A aritmética funciona como você esperaria. Comparações retornam booleanos, e os operadores lógicos && / || fazem curto-circuito - o lado direito é totalmente ignorado quando o esquerdo já decide o resultado.
17 % 5 -- Output: 2 (módulo / resto) 2 + 3 * 4 -- Output: 14 (precedência usual) 3 > 2 -- Output: true 5 >= 5 -- Output: true false && (1 / 0) -- Output: false (o lado direito não executa, então sem erro) true || (1 / 0) -- Output: true
A indexação negativa conta a partir do fim de uma lista ou string - -1 é o último elemento:
[10, 20, 30][-1] -- Output: 30 [10, 20, 30][-2] -- Output: 20 "Bern"[-1] -- Output: 'n'
Todo símbolo tem um sinônimo em palavras simples, então um programa pode ser lido como uma frase. O símbolo e a palavra são idênticos para o parser - escolha o que for mais claro e misture-os livremente.
| Símbolo | Palavra | Símbolo | Palavra |
|---|---|---|---|
+ | plus | == | equals / is |
- | minus | != | not-equals |
* | times | && | and |
/ | divided-by | || | or |
% | modulo | ! | not |
> | is-greater | <> | concat |
>= | is-greater-or-equal | )| | and-do |
= | be | -> | returns / such-that |
O mesmo exemplo, duas vezes. Pressione Show written form em qualquer um dos blocos para confirmar que são intercambiáveis:
3 > 2 && 5 <= 5 [1, 2] <> [3, 4] total = 10 + 5
3 is-greater 2 and 5 is-less-or-equal 5 [1, 2] concat [3, 4] total be 10 plus 5
Laços também aceitam in como sinônimo de :, e corpos de função aceitam returns no lugar de ->.
A forma if / then / else / end escolhe entre ramos. Encadeie casos extras com else if.
age = 18
if age >= 18 then
"You are an adult."
else
"You are a minor."
end
-- Output: "You are an adult."
score = 85
if score >= 90 then
"You got an A."
else if score >= 80 then
"You got a B."
else if score >= 70 then
"You got a C."
else
"You need to improve."
end
-- Output: "You got a B."
Condicionais aninham naturalmente. Um FizzBuzz clássico sobre um intervalo:
loop n : [1..15] do
if n % 15 == 0 then
"FizzBuzz"
else if n % 3 == 0 then
"Fizz"
else if n % 5 == 0 then
"Buzz"
else
n
end
end
if como ExpressãoComo if produz um valor, você pode usá-lo diretamente dentro de um corpo de função ou atribuição - sem precisar de variável temporária.
def sign(n) -> if n > 0 then "positive" else if n < 0 then "negative" else "zero" end sign(7) -- Output: "positive" sign(-3) -- Output: "negative" sign(0) -- Output: "zero"
Funciona até em linha, sem nenhuma função ao redor:
label = if true then 1 else 2 end -- label é 1
Inspirada em Odin, Bern tem uma única palavra-chave de laço, loop. O que você coloca depois dela muda o seu comportamento. (for é aceito em todos os lugares como um sinônimo retrocompatível.)
Dê a ele um número para repetir um corpo essa quantidade de vezes.
loop 3 do
"Hello, Loops!"
end
Dê a ele uma condição booleana e ele repete enquanto essa condição permanecer verdadeira - este é o while do Bern.
counter = 2
loop counter > 0 do
"Hello, Loops!"
counter = counter - 1
end
loop true repete para sempre - útil para servidores e programas no estilo REPL (interrompa com break ou saindo).
loop true do
"Hello!"
end
Itere os elementos de uma lista, conjunto ou string com : (ou a palavra in).
text = "Bern"
loop char : text do
"Current char is: " + char
end
Peça uma segunda variável e Bern lhe entrega a posição ao lado de cada elemento.
text = "Bern"
loop char, index : text do
"Char " + char + " is at index " + index
end
As funções mais simples são de uma linha: def name(params) -> expression. A expressão depois da seta é o valor de retorno.
def add(x, y) -> x + y add(2, 3) -- Output: 5
Para lógica de várias etapas, use um bloco do ... end e um return explícito.
def sumList(xs) do
total = 0
loop n : xs do
total = total + n
end
return total
end
sumList([1, 2, 3, 4])
-- Output: 10
Uma função pode ter várias cláusulas. Bern as tenta de cima para baixo e executa a primeira cujo padrão casa com os argumentos. Isso substitui a maioria das cadeias de if.
def sign(0) -> "zero" def sign(n) -> "positive" sign(0) -- Output: "zero" sign(42) -- Output: "positive"
Padrões podem ser literais (casam exatamente), variáveis (vinculam qualquer coisa) ou _ (o curinga, que casa e ignora).
def greet("Alice") -> "Hi, Alice!"
def greet(_) -> "Hello, someone else!"
greet("Alice") -- Output: "Hi, Alice!"
greet("Bob") -- Output: "Hello, someone else!"
Números negativos funcionam como padrões literais, e uma variável repetida em um mesmo padrão força os valores casados a serem iguais:
def describe(-1) -> "minus one" def describe(_) -> "something else" def same(x, x) -> "equal" def same(_, _) -> "different" same(3, 3) -- Output: "equal" same(3, 4) -- Output: "different"
Listas são desestruturadas com o padrão [head | tail] - a base das funções recursivas sobre listas:
def first([]) -> error("first: empty")
def first([h | _]) -> h
first([10, 20]) -- Output: 10
first([]) -- Output: um valor de erro (veja "Erros como Valores")
Adicione when <condição> depois dos parâmetros para que uma cláusula só se aplique quando a condição também for verdadeira. Guardas permitem que um conjunto de padrões se divida pelo conteúdo de um valor.
def classify(n) when n > 0 -> "positive" def classify(0) -> "zero" def classify(n) -> "negative" classify(7) -- Output: "positive" classify(0) -- Output: "zero" classify(-3) -- Output: "negative"
Guardas funcionam com corpos em bloco e com lambdas também:
def grade(n) when n >= 90 do return "A" end def grade(n) when n >= 80 -> "B" def grade(_) -> "C" check_pos = \x when x > 0 -> "yes" check_pos(4) -- Output: "yes"
Funções são valores comuns. Escreva uma anônima com \params -> expression, guarde-a em uma variável, passe-a adiante ou retorne-a.
inc = \x -> x + 1 pairSwap = \a, b -> [b, a] inc(10) -- Output: 11 pairSwap(1, 2) -- Output: [2, 1]
Uma função de ordem superior recebe outra função como argumento. map e filter de core são os exemplos do dia a dia:
import core map([1, 2, 3, 4, 5], \x -> x * 2) -- Output: [2, 4, 6, 8, 10] def applyTwice(f, x) -> f(f(x)) applyTwice(\n -> n * 2, 5) -- Output: 20
Chame uma função com menos argumentos do que ela espera e Bern retorna uma nova função aguardando o restante. Isso facilita construir auxiliares especializados na hora.
import core def add(x, y) -> x + y add2 = add(2) -- uma nova função: "soma 2 ao seu argumento" add2(10) -- Output: 12 add(2)(40) -- Output: 42 (encadeia as chamadas diretamente)
A aplicação parcial brilha ao alimentar auxiliares para map e companhia:
import core def add(x, y) -> x + y map([10, 20, 30], add(1)) -- Output: [11, 21, 31]
x )| f passa x como o primeiro argumento de f - isto é, x )| f é exatamente f(x). Ele faz as transformações de dados serem lidas da esquerda para a direita em vez de de dentro para fora.
import core [3, 1, 2] )| reverse -- Output: [2, 1, 3]
Quando a função precisa de mais argumentos, o valor encaminhado ocupa o primeiro lugar e você fornece o resto:
import core [1, 2, 3, 4] )| filter(\x -> x > 2) -- o mesmo que filter([1, 2, 3, 4], \x -> x > 2) -> [3, 4]
Pipes são associativos à esquerda, então o encadeamento constrói um pipeline legível:
import core [1, 2, 3, 4] )| filter(\x -> x % 2 == 0) )| map(\x -> x * 10) -- Output: [20, 40]
2 + 3 )| is_five o 2 + 3 é avaliado primeiro e depois encaminhado. Você também pode encaminhar direto para uma lambda: 5 )| (\x -> x + 1).
Listas são coleções ordenadas escritas com colchetes. A sintaxe de intervalo [a..b] constrói uma lista de a até b inclusive.
numbers = [1, 2, 3, 4, 5] range = [5..10] -- range é [5, 6, 7, 8, 9, 10]
Bern sobrecarrega operadores aritméticos e de teoria dos conjuntos para listas:
[1, 2] + 2 -- Output: [3, 4] (soma a cada elemento) [1, 2] <> [3, 4, 5] -- Output: [1, 2, 3, 4, 5] (concatenação) [1, 2, 3] <| [3, 4, 5] -- Output: [1, 2, 3, 4, 5] (união, sem duplicatas)
| Operador | Palavra | Significado |
|---|---|---|
<> | concat | Junta duas coleções ponta a ponta |
<| | union | Todos os elementos de ambas, sem duplicatas |
|> | intersect | Somente os elementos presentes em ambas |
</> | difference | Elementos na primeira, mas não na segunda |
Conjuntos usam chaves e mantêm seus elementos únicos automaticamente. Eles oferecem os mesmos operadores de teoria dos conjuntos das listas.
{1, 2} + 3 -- Output: {1, 2, 3} (adiciona um elemento)
{1, 2, 3} |> {2, 3, 4} -- Output: {2, 3} (interseção)
{1, 2, 3} > {2, 3, 4} -- Output: {1} (diferença)
Objetos mapeiam chaves para valores usando a notação #{ ... }#.
obj = #{
key: "value",
hello: "world"
}#
obj
Indexe com colchetes para ler, reatribuir ou adicionar novas chaves:
obj["key"] = "new_value" -- reatribui uma chave existente obj["new_key"] = "added" -- adiciona uma chave totalmente nova
Objetos aninham, e você pode encadear índices para alcançar o interior:
obj["inner"] = #{ nested_key: "nested_value" }#
obj["inner"]["nested_key"]
-- Output: "nested_value"
Um ADT define um tipo como uma escolha entre vários construtores, cada um carregando seus próprios campos. Declare um com adt:
adt Shape = Circle Double | Rectangle Double Double
Construtores são chamados como funções para construir valores:
c = Circle(5.0) r = Rectangle(3.0, 4.0)
O casamento de padrões os desestrutura, extraindo os campos pelo nome. É aqui que ADTs e funções de múltiplas cláusulas se combinam lindamente:
def area(Circle(r)) -> 3.14159 * r * r def area(Rectangle(w, h)) -> w * h area(c) -- Output: 78.53975 area(r) -- Output: 12.0
Um uso comum de ADTs é modelar "um valor, ou nada" com segurança - sem surpresas de null. Aqui uma divisão retorna None em vez de falhar ao dividir por zero:
adt Maybe = Just Int | None def safeDivide(n, 0) -> None() def safeDivide(n, m) -> Just(n / m) safeDivide(10, 3) -- Output: Just(3) safeDivide(5, 0) -- Output: None()
caseQuando você quer casamento de padrões dentro de uma expressão em vez de entre cláusulas de função, use case value is pattern = result | ... end. Cada ramo pode carregar a sua própria guarda.
adt Shape = Circle Double | Square Double
def check(v) -> case v is
Circle(_) = "It's a Circle!"
| Square(_) = "It's a Square!"
| _ = "Unknown"
end
check(Circle(5.0)) -- Output: "It's a Circle!"
check(Square(2.0)) -- Output: "It's a Square!"
Ramos podem vincular campos e retorná-los, ou casar pelo resultado de :: (typeof). As guardas usam a mesma palavra-chave when das cláusulas de função:
def size(n) -> case n is
x when x > 100 = "big"
| x when x > 10 = "medium"
| _ = "small"
end
size(500) -- Output: "big"
size(50) -- Output: "medium"
size(5) -- Output: "small"
A biblioteca json faz o parse de texto JSON para valores Bern e os serializa de volta.
import json
person = json_parse("{\"name\": \"Bern\", \"version\": 3}")
person["name"] -- Output: "Bern"
person["version"] -- Output: 3
Objetos JSON viram objetos Bern; o null do JSON vira Undefined (teste com is_null); e json_stringify transforma um valor de volta em uma string:
import json
is_null(json_parse("null"))
-- Output: true
json_stringify(json_parse("{\"items\": [1, 2, 3], \"ok\": true}"))
-- Output: uma string JSON
[1, "a", 34.2, "a"] é convertido em {1, ["a", "a"], 34.2}, e retorna ao array original na ida e volta.
Em vez de falhar, as falhas de execução em Bern produzem um valor de erro que você pode inspecionar com is_error. Isso mantém o programa rodando e deixa você decidir o que fazer.
import core is_error(undefined_fn(1)) -- Output: true (função inexistente) is_error([1, 2, 3][99]) -- Output: true (índice fora dos limites) is_error(head([])) -- Output: true (nenhuma cláusula casa) is_error(42) -- Output: false (um valor comum)
Erros são "veneno": eles se propagam pelos operadores, então uma única checagem no final diz se toda a computação teve sucesso. Você também pode criar o seu próprio com error(...).
import core
is_error(undefined_fn(1) + 100) -- Output: true (o veneno se espalha pelo +)
oops = error("boom")
is_error(oops) -- Output: true
-- e o trabalho normal continua sem ser afetado depois
7 * 6 -- Output: 42
Intervalos são preguiçosos: [1..1000000000] é criado instantaneamente e só calcula os elementos conforme você os pede. Comprimento, indexação e take são todos O(1) ou proporcionais ao que você de fato usa.
import core big = [1..1000000000] :> big -- Output: 1000000000 (instantâneo, sem alocação) big[5] -- Output: 6 take(5, big) -- Output: [1, 2, 3, 4, 5]
Intervalos pequenos se comportam exatamente como listas comuns, então você nunca precisa pensar em qual deles tem:
import core [1..5] -- Output: [1, 2, 3, 4, 5] map([1..4], \x -> x * 2) -- Output: [2, 4, 6, 8]
Traga funcionalidades extras com import. A biblioteca core contém os auxiliares de ordem superior do dia a dia, como map, filter e reverse.
import core map([1, 2, 3, 4, 5], \x -> x + 2) -- Output: [3, 4, 5, 6, 7] def isEven(x) -> x % 2 == 0 filter([1, 2, 3, 4, 5, 6], isEven) -- Output: [2, 4, 6]
Outras bibliotecas incluem math, strings, random, json, assert e mais - veja a referência da biblioteca padrão para a lista completa.
input() lê uma linha do usuário, bem parecido com o input do Python. Ele sempre retorna texto; converta-o com auxiliares de core, como to_int.
name = input("Type your name: ")
name
-- Output: o que você digitou, como uma string
import core
age_text = input("Type your age: ")
age = to_int(age_text)
"In five years you'll be " + (age + 5)