Instale, abra o REPL e acompanhe - da sua primeira expressão até os seus próprios programas.
Boas-vindas ao Bern! Colocá-lo na sua máquina leva uma única linha - escolha o shell que você usa, cole e está pronto para brincar.
iex (irm https://bern-lang.github.io/Bern/install/install_bern.ps1)
curl -fsSL https://bern-lang.github.io/Bern/install/install_bern.sh | bash
Para conferir se deu certo, abra um terminal e digite bern sem argumentos. Isso te leva direto para o prompt interativo - que é exatamente para onde vamos a seguir, e a forma mais amigável de aprender a linguagem.
O REPL é o melhor lugar para começar - e o melhor lugar para voltar sempre. Execute bern sem argumentos e você ganha um prompt interativo que lê uma linha, avalia e imprime o resultado, na hora. Não há nada para configurar nem nada para quebrar: digite algo, veja o que acontece, então tente a próxima coisa. Erros de parse ou de execução são simplesmente impressos, e a sessão continua.
Aqui está a ideia inteira em quatro linhas - vá em frente e digite:
[bern]: 1 + 2 3 [bern]: "Hello, " + "Bern!" "Hello, Bern!"
Todo exemplo deste guia é uma linha que você pode colar direto nesse prompt. Conforme você lê, faça exatamente isso - brincar com cada ideia é a forma mais rápida de fixá-la.
_O resultado da última expressão é vinculado a _, então você pode construir a partir dele:
[bern]: 1 + 2 3 [bern]: _ * 10 30
Construções em bloco (def ... do ... end, for ... do ... end e assim por diante) são lidas ao longo de várias linhas até ficarem completas, mostradas com um prompt de continuação:
[bern]: def addy(x, y) do
...: return x + y
...: end
[bern]: addy(3, 4)
7
| Comando | Ação |
|---|---|
:help, :h | Mostra a lista de comandos |
:load <file>, :l | Carrega e executa um arquivo .brn na sessão atual |
:reset | Limpa as definições locais |
:quit, :q | Sai (ou pressione Ctrl-D) |
O histórico de comandos está disponível com as setas para cima/baixo e é salvo entre sessões.
É aqui que a diversão começa. Tudo abaixo é algo que você pode digitar no REPL agora mesmo e ver acontecer - então deixe esse prompt aberto e acompanhe. Vamos construir do simples ao completo, de valores e variáveis até funções e os seus próprios programinhas, uma pequena ideia de cada vez.
Em Bern, literais são interpretados e impressos automaticamente. Você pode escrever valores diretamente e eles serão avaliados na hora:
"Hello, World!" 23 3.14 true
Variáveis são definidas usando a sintaxe simples name = value. Bern suporta vários tipos de dados, incluindo inteiros, doubles, strings e booleanos:
integer = 2 double = 3.14 helloworld = "Hello, World!" isActive = true
Os números fazem exatamente o que você espera. São cinco operadores aritméticos, e você pode testar cada um deles no REPL agora mesmo:
7 + 2 -- 9 (soma) 7 - 2 -- 5 (subtração) 7 * 2 -- 14 (multiplicação) 7 / 2 -- 3 (divisão) 7 % 2 -- 1 (resto, ou "módulo")
Uma coisa para saber sobre a divisão: entre dois números inteiros ela divide e descarta o resto, mas um decimal em qualquer lugar mantém a fração.
7 / 2 -- 3 (divisão inteira) 7.0 / 2 -- 3.5 (um double torna exato)
* e / prendem mais forte que + e -, como na matemática, e os parênteses sobrepõem isso:
2 + 3 * 4 -- 14 (o 3 * 4 acontece primeiro) (2 + 3) * 4 -- 20
Todo operador também tem um nome em palavras (7 plus 2, 7 times 2) - veja Operadores para o conjunto completo, incluindo o pipe.
Assim como literais, variáveis podem ser chamadas diretamente e serão devidamente avaliadas. Basta escrever o nome da variável para ver o seu valor:
helloworld -- Output: "Hello, World!" integer -- Output: 2
Bern oferece dois operadores especiais para introspecção:
:: (typeof) - Retorna o tipo de um valor como uma string:> (length) - Retorna o comprimento de uma lista, conjunto ou stringtypeof_integer = :: 2 -- Output: "Integer" typeof_string = :: "hello" -- Output: "List" (uma string é uma lista de caracteres) length_list = :> [1,2,3,4] -- Output: 4 length_string = :> "hello" -- Output: 5
Veja a seção Tipos para a lista completa de valores que :: pode reportar.
Bern é de tipagem dinâmica: uma variável pode guardar um valor de qualquer tipo, e os tipos são verificados enquanto o programa roda, em vez de declarados antecipadamente. Você nunca precisa anotar o tipo de uma variável. Ainda assim, vale conhecer exatamente quais tipos existem, porque o operador :: (typeof) os reporta pelo nome e o casamento de padrões raciocina sobre eles - e porque os ADTs são construídos sobre eles.
Todo valor em Bern é um dos seguintes. A coluna nome no typeof é a string exata que :: retorna para aquele valor.
| Tipo | nome no typeof | Exemplo | Descrição |
|---|---|---|---|
| Integer | "Integer" | 2, -42, 0 | Números inteiros. |
| Double | "Double" | 3.14, -2.5, 1.0 | Números de ponto flutuante. |
| Boolean | "Boolean" | true, false | Valores lógicos. |
| Character | "Character" | 'a', 'Z' | Um único caractere, escrito com aspas simples. |
| List | "List" | [1, 2, 3], "olá" | Coleção ordenada. Uma string é uma lista de caracteres, então também reporta "List". |
| Set | "Set" | {1, 2, 3} | Coleção não ordenada sem duplicatas. Veja Conjuntos. |
| Object | "Object" | #{ key: "v" }# | Mapa de chave-valor. Veja Objetos. |
| Function | "Function" | def add(x, y) -> x + y | Uma função nomeada definida com def. |
| Lambda | "Lambda" | \x -> x + 1 | Uma função anônima. Veja Funções. |
| NaN | "NaN" | 0.0 / 0.0 | "Not a Number" - um resultado numérico indefinido. |
| Undefined | "Undefined" | variável não definida | A ausência de um valor (ex.: um nome não vinculado, ou null de JSON). |
| Error | "Error" | error("boom") | Um erro de execução carregado como valor. Veja Erros como Valores. |
| ADT | o próprio nome | Circle(5.0) | Um valor definido pelo usuário. Reporta o próprio nome do tipo, ex.: "Shape". Veja ADTs. |
Use :: (ou sua forma em palavra typeof) para perguntar a qualquer valor seu tipo em tempo de execução. O resultado é uma string, então você pode compará-lo ou ramificar sobre ele:
:: 42
-- Output: "Integer"
:: 3.14
-- Output: "Double"
:: true
-- Output: "Boolean"
:: 'a'
-- Output: "Character"
:: [1, 2, 3]
-- Output: "List"
:: {1, 2, 3}
-- Output: "Set"
:: #{ name: "Bern" }#
-- Output: "Object"
:: "olá" reporta "List", e não um tipo de string separado. Da mesma forma, padrões de inteiro e double casam entre si - 5 casa com 5.0 em uma cláusula de função.
Quando você declara um ADT, cada campo do construtor é anotado com um tipo. Os nomes aceitos em uma declaração adt são escritos na forma curta e formam um vocabulário separado dos nomes do typeof acima:
Na declaração adt | Reportado por :: como | Significado |
|---|---|---|
Int | "Integer" | Campo de número inteiro. |
Double | "Double" | Campo de ponto flutuante. |
Bool | "Boolean" | Campo booleano. |
Char | "Character" | Campo de um único caractere. |
String | "List" | Campo de texto (uma lista de caracteres). |
List | "List" | Campo de lista. |
Set | "Set" | Campo de conjunto. |
Auto / Any | o tipo do próprio valor | Bern 2.1 Campo dinâmico: aceita um valor de qualquer tipo, detectado automaticamente. Ideal para uma linguagem de tipagem dinâmica. |
| qualquer nome | esse nome | Outro ADT usado como tipo de campo (ex.: Shape). |
adt Shape = Circle Double | Rectangle Double Double | Named String adt Tree = Leaf Int | Node Tree Tree -- ADTs podem aninhar outros ADTs adt Box = Box Auto -- o campo guarda o tipo que for passado
Os tipos de campo são usados para verificar a aridade de cada construtor quando o arquivo é carregado; eles não são estritamente impostos sobre os valores em tempo de execução, em consonância com a natureza dinâmica de Bern. O tipo Auto torna esse dinamismo explícito: use-o quando um campo de construtor deve aceitar qualquer valor.
typed? Clique aqui! Bern 2.1Com o pragma typed ativado, você pode anotar uma variável com um tipo usando nome :: Tipo = valor. O valor é verificado contra o tipo declarado quando a atribuição executa; uma incompatibilidade é um erro fatal. É opcional - sem o pragma, a sintaxe fica inativa e :: mantém seu significado usual de operador typeof.
{--! typed !--}
x :: Integer = 10
a :: Character = 'a'
name :: String = "bern"
pi :: Double = 3.14
ok :: Boolean = true
bad :: Integer = 'a'
-- Error: type mismatch for 'bad': declared Integer but value is Character
O tipo declarado usa os nomes canônicos do typeof (Integer, Double, Boolean, Character, String, List, Set, Object), e alguns apelidos familiares também são aceitos: Int, Float, Char, Bool, Text. Um nome de tipo ADT (ex.: Shape) casa com um valor construído por qualquer um de seus construtores. O tipo Auto (ou Any) aceita qualquer valor:
{--! typed !--}
qualquer :: Auto = 7
qualquer :: Auto = "agora uma string" -- Auto nunca reclama
adt Shape = Circle Double | Square Double
sh :: Shape = Circle(1.0) -- um valor Shape é aceito
Além dos símbolos que você já conhece, o Bern dá à maioria das operações uma forma escrita em palavras também, para que um programa se leia como prosa.
Todo operador simbólico também tem uma forma em palavras, então os programas podem ser lidos como prosa. O símbolo e a palavra são intercambiáveis.
| Símbolo | Palavra | Símbolo | Palavra |
|---|---|---|---|
+ | plus | == | equals (ou is) |
- | minus | != | not-equals |
* | times | && | and |
/ | divided-by | || | or |
% | modulo | ! | not |
> | is-greater | <> | concat |
< | is-less | <| | union |
>= | is-greater-or-equal | |> | intersect |
<= | is-less-or-equal | </> | difference |
:: | typeof | )| | and-do |
:> | length | = | be (atribuição) |
Laços também aceitam in como sinônimo de : (e for como sinônimo de loop), e corpos de função e de case aceitam returns como sinônimo de -> / =.
nums be [1, 2, 3, 4, 5] 3 is-greater 2 and 5 is-less-or-equal 5 -- Output: true [1, 2] concat [3, 4] -- Output: [1, 2, 3, 4] def classify(n) when n is-greater 0 returns "positive"
is-greater-or-equal) e se resolvem da mesma forma que seus símbolos, com a mesma precedência.
Todo nome em Bern vive em um escopo - a região do programa onde aquele vínculo pode ser visto. Bern mantém dois tipos de escopo, e o operador que você usa para atribuir uma variável decide em qual deles ela entra:
= (ou a palavra be) - um vínculo local, visível apenas no escopo atual.:= - um vínculo global, visível em todo o programa.=)A atribuição comum nome = valor cria um vínculo no escopo atual. No nível superior de um arquivo ou no REPL, esse escopo é o próprio arquivo. Dentro do corpo de uma função, laço ou ramo condicional, o corpo tem seu próprio escopo: vínculos criados ali com = são locais e desaparecem quando o corpo termina - eles não vazam de volta para quem chamou.
def f() do
local = 10 -- vinculado apenas dentro de f
return 0
end
x = f()
print local -- indefinido: `local` nunca escapou de f
Cada chamada recebe um novo escopo local, então locais de uma chamada nunca invadem outra. É isso que torna as funções autocontidas: um auxiliar pode usar os nomes temporários que quiser sem perturbar os nomes ao redor do local da chamada.
:=)O operador := escreve em uma única tabela global de todo o programa. Um global atribuído em qualquer lugar - mesmo no fundo de uma função - torna-se visível para todos os outros escopos e sobrevive depois que a função que o criou retorna.
def f() do
shared := 20 -- escrito no escopo global
return 0
end
x = f()
print shared -- Saída: 20 (o global sobreviveu à chamada)
Use := quando você deliberadamente quiser um estado que sobreviva a um único escopo - um contador, um valor de configuração ou um resultado que várias funções precisam compartilhar. Para todo o resto, prefira =: vínculos locais mantêm seu programa mais fácil de acompanhar porque seus efeitos permanecem contidos.
= vs := em Resumo= (local) | := (global) | |
|---|---|---|
| Onde vincula | Apenas no escopo atual | Tabela global de todo o programa |
| Visível dentro de outras funções | Não | Sim |
| Sobrevive após o fim do escopo | Não | Sim |
| Uso típico | Valores temporários / de trabalho | Estado compartilhado e duradouro |
Quando Bern lê uma variável, ele procura primeiro no escopo global e só recorre ao escopo local se o nome não for global. Isso significa que uma vez que um nome foi vinculado com :=, um = local posterior com o mesmo nome não consegue sobrepô-lo - as leituras continuam retornando o valor global:
g := 1 g = 2 -- escreve um `g` local, mas... print g -- Saída: 1 (o global é encontrado primeiro)
:= se você realmente quiser mudar o valor compartilhado.
Bern suporta execução condicional por meio de instruções if-then-else. Elas permitem que seu código tome decisões com base em condições.
A estrutura condicional básica permite executar códigos diferentes conforme uma condição seja verdadeira ou falsa:
age = 18
if age >= 18 then
"You are an adult."
else
"You are a minor."
end
-- Output: "You are an adult."
Para múltiplas condições, use else if para verificar casos adicionais em sequência:
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."
Um desafio clássico de programação demonstrando condicionais aninhadas:
numbers = [1..100]
loop n : numbers 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
Os operadores booleanos && e || param no primeiro operando que decide o resultado, então o lado direito só é avaliado quando necessário.
false && (1 / 0 == 0) -- Output: false (the right side never runs)
Bern usa uma sintaxe de laço inspirada em Odin, onde loop é a única palavra-chave de laço. O comportamento muda conforme você a usa.
for é aceito em todos os lugares onde loop é, como um sinônimo retrocompatível. loop é a grafia preferida.
Executa um bloco um número específico de vezes:
loop 3 do
"Hello, Loops!"
end
-- Output: "Hello, Loops!" (three times)
Repete enquanto uma condição for verdadeira (como um laço while):
counter = 2
loop counter > 0 do
"Hello, Loops!"
counter = counter - 1
end
-- Output: "Hello, Loops!" (twice)
Crie um laço infinito com loop true:
loop true do
"This runs forever!"
end
Itere sobre os elementos de listas, conjuntos ou strings:
text = "Bern"
loop char : text do
"Current char is: " + char
end
-- Output:
-- "Current char is: B"
-- "Current char is: e"
-- "Current char is: r"
-- "Current char is: n"
Obtenha tanto o elemento quanto o seu índice durante a iteração:
text = "Bern"
loop char, index : text do
"Char: " + char + " at index: " + index
end
-- Output:
-- "Char: B at index: 0"
-- "Char: e at index: 1"
-- "Char: r at index: 2"
-- "Char: n at index: 3"
Uma lista é uma coleção ordenada de elementos do mesmo tipo. Listas permitem duplicatas e preservam a ordem de inserção. Em Bern, strings também são tratadas como listas de caracteres.
numbers_list = [1,2,3,4,5] numbers_list -- Output: [1,2,3,4,5]
Crie listas usando a notação de intervalo no estilo Haskell:
range_list = [5..10] -- Output: [5,6,7,8,9,10] small_range = [1..5] -- Output: [1,2,3,4,5]
Acesse elementos específicos usando indexação baseada em zero:
range_list = [5..10] range_list[3] -- Output: 8 (fourth element)
Operações aritméticas com um escalar se aplicam a cada elemento:
[1,2] + 2 -- Output: [3,4] [10,20,30] * 2 -- Output: [20,40,60]
Operações entre duas listas exigem comprimentos iguais:
[1,2,3] + [4,5,6] -- Output: [5,7,9] [10,20] - [5,3] -- Output: [5,17]
Listas suportam operações de conjunto preservando a ordem e permitindo duplicatas:
<> - União (concatenação)</> - Diferença (elementos na primeira, mas não na segunda)|> - Interseção (elementos em comum)<| - Diferença simétrica (elementos em uma ou outra, mas não em ambas)[1,2,3] <> [3,4,5] -- Output: [1,2,3,3,4,5] [1,2,3] > [2,3] -- Output: [1] [1,2,3] |> [2,3] -- Output: [2,3] [1,2,3] <| [2,3] -- Output: [1]
result = ([1,2,3,4] <> [3..5]) <| ([2,3,4] |> [4,5]) > [1] -- Combines concatenation, symmetric difference, -- intersection, and regular difference
Índices negativos contam a partir do fim de uma lista ou string.
[10, 20, 30][-1] -- Output: 30 "Bern"[-1] -- Output: 'n'
Intervalos são preguiçosos: seu comprimento é calculado instantaneamente e somente os elementos que você de fato acessa são materializados, então até mesmo um intervalo enorme é barato de usar.
big = [1..1000000000] :> big -- Output: 1000000000 (instant, nothing is materialised) take(5, big) -- Output: [1, 2, 3, 4, 5]
Em um padrão - uma cláusula de função ou um ramo de case - a sintaxe [head | tail] divide uma lista em seu primeiro elemento e o restante. Isso se chama padrão cons. O nome antes do | vincula ao primeiro elemento; o nome depois dele vincula a uma lista com todo o resto.
def head([h | t]) -> h def tail([h | t]) -> t head([10, 20, 30]) -- Output: 10 tail([10, 20, 30]) -- Output: [20, 30]
Use _ para qualquer parte que você não precise, e adicione uma cláusula de lista vazia [] para que a função seja exaustiva (um padrão cons nunca casa com uma lista vazia):
def first([h | _]) -> h
def first([]) -> error("first: empty list")
first([1, 2, 3])
-- Output: 1
first([])
-- Output: um valor de erro
Como o tail é, ele mesmo, uma lista, os padrões cons são a forma natural de escrever funções recursivas que percorrem uma lista um elemento de cada vez:
def sum([]) -> 0 def sum([h | t]) -> h + sum(t) sum([1, 2, 3, 4]) -- Output: 10
Para extrair mais de um elemento inicial, aninhe o padrão - [a | [b | rest]] vincula os dois primeiros elementos como a e b. Conjuntos têm a mesma construção com chaves, {head | tail}, embora, como conjuntos não sejam ordenados, o "primeiro" elemento seja simplesmente aquele que o conjunto produzir primeiro.
[head | tail] também funciona como expressão - veja Expressões Cons abaixo.
Em posição de expressão, [head | tail] constrói uma lista adicionando um ou mais elementos no início de uma já existente - o espelho do padrão cons. Os valores antes do | viram a nova frente; a expressão depois dele precisa avaliar para uma lista.
[0 | [1, 2, 3]] -- Output: [0, 1, 2, 3] [1, 2 | [3, 4]] -- Output: [1, 2, 3, 4] (vários elementos no início são permitidos) x = [9 | [10, 11]] -- x é [9, 10, 11] (sim, é uma expressão de verdade que você pode atribuir)
A adição no início é preguiçosa: o tail nunca é forçado, então você pode fazer cons sobre um intervalo enorme e ainda obter um resultado instantâneo.
big = [0 | [1..1000000000]] :> big -- Output: 1000000001 (instantâneo) big[1] -- Output: 1
Conjuntos têm a forma correspondente {head | tail}, onde o tail precisa ser um conjunto.
Uma list comprehension constrói uma lista a partir de um ou mais geradores (padrão <- fonte) e guardas opcionais (filtros booleanos), escrita como [expressão | qualificadores]. Lê-se como "a expressão, para cada vínculo produzido pelos qualificadores".
[x * x | x <- [1..5]] -- Output: [1, 4, 9, 16, 25] [x | x <- [1..10], x % 2 == 0] -- Output: [2, 4, 6, 8, 10] (uma guarda mantém apenas os x pares)
Vários geradores aninham como laços, produzindo todas as combinações (o último gerador varia mais rápido):
[a + b | a <- [1, 2], b <- [10, 20]] -- Output: [11, 21, 12, 22]
O lado esquerdo de um gerador é um padrão completo, então ele pode desestruturar enquanto itera. Elementos que não casam com o padrão são ignorados - uma forma prática de filtrar e desembrulhar de uma vez:
[h | [h | _] <- [[1, 2], [], [3]]] -- Output: [1, 3] (a lista vazia não tem head, então é descartada)
Como uma string é uma lista de caracteres, você pode iterá-la diretamente:
[c | c <- "abc"] -- Output: ['a', 'b', 'c']
<-, [a | b] é lido como uma expressão cons. O estilo clássico com map/filter/pipe também continua funcionando, e muitas vezes é mais claro para uma única transformação: [1..10] )| filter(\x -> x % 2 == 0) )| map(\x -> x * x).
Um conjunto é uma coleção não ordenada de elementos únicos que podem ser de tipos diferentes. Conjuntos removem duplicatas automaticamente e não preservam a ordem.
numbers_set = {1,2,3,4,5}
numbers_set
-- Output: {1,2,3,4,5}
mixed_set = {1, "hello", 3.14}
-- Sets can contain different types
Embora conjuntos não sejam ordenados, você ainda pode acessar elementos por índice:
numbers_set = {1,2,3,4,5}
numbers_set[2]
-- Output: (some element from the set)
Adicionar a um conjunto insere elementos (se ainda não estiverem presentes):
{1,2} + 3
-- Output: {1,2,3}
{1,2} + {3,4}
-- Output: {1,2,3,4}
{1,2,3} + 2
-- Output: {1,2,3} (2 already exists)
A subtração remove elementos:
{1,2,3} - 2
-- Output: {1,3}
Operações matemáticas de conjunto com remoção automática de duplicatas:
{1,2,3} <> {3,4,5}
-- Union: {1,2,3,4,5}
{1,2,'a',3.5} <> {3,4,'b',1}
-- Union with mixed types: {1,2,'a',3.5,3,4,'b'}
{1,2,3} |> {2,3,4}
-- Intersection: {2,3}
{1,2,3} > {2,3,4}
-- Difference: {1}
{1,2,3} <| {2,3}
-- Symmetric difference: {1}
Bern suporta objetos (hashmaps/hashtables) usando a notação #{...}# para pares chave-valor.
obj = #{
key: "value",
hello: "world"
}#
obj
-- Output: #{key: "value", hello: "world"}#
Reatribua valores usando a notação de colchetes:
obj["key"] = "new_value"
obj
-- Output: #{key: "new_value", hello: "world"}#
Adicione novos pares chave-valor atribuindo a uma nova chave:
obj["new_key"] = "added_value"
obj
-- Output: #{key: "new_value", hello: "world", new_key: "added_value"}#
Objetos podem conter outros objetos. Acesse valores aninhados com indexação múltipla:
obj["test"] = #{
nested_key: "nested_value"
}#
obj["test"]["nested_key"]
-- Output: "nested_value"
person = #{
name: "Alice",
age: 25,
address: #{
city: "Boston",
zip: "02101"
}#
}#
person["address"]["city"]
-- Output: "Boston"
As funções de Bern são poderosas e flexíveis, suportando casamento de padrões, sintaxe de seta em linha e corpos em bloco com returns explícitos.
Para funções de uma linha, use a sintaxe de seta def name(params) -> expr:
def add(x, y) -> x + y add(2, 3) -- Output: 5 def square(x) -> x * x square(4) -- Output: 16
Defina várias cláusulas para uma função. Bern usará o primeiro padrão que casar:
def sign(0) -> "zero" def sign(n) -> "positive" sign(0) -- Output: "zero" sign(42) -- Output: "positive"
Use _ para ignorar parâmetros, ou case valores literais específicos:
def greet("Alice") -> "Hi, Alice!"
def greet(_) -> "Hello, someone else!"
greet("Alice")
-- Output: "Hi, Alice!"
greet("Bob")
-- Output: "Hello, someone else!"
Para funções complexas, use blocos do ... end com instruções return explícitas:
def sumList(xs) do
total = 0
loop n : xs do
total = total + n
end
return total
end
sumList([1,2,3])
-- Output: 6
Funções anônimas usam a sintaxe de barra invertida e podem ser atribuídas a variáveis ou passadas como argumentos:
inc = \x -> x + 1 pairSwap = \a, b -> [b, a] inc(10) -- Output: 11 pairSwap(1, 2) -- Output: [2, 1]
Funções podem aceitar outras funções como parâmetros:
def applyTwice(f, x) -> f(f(x)) applyTwice(\n -> n * 2, 5) -- Output: 20 (5 * 2 * 2)
when)Uma cláusula casa apenas quando seus padrões casam e sua guarda when avalia para true. Se a guarda for falsa, a avaliação segue para a próxima cláusula. Guardas funcionam em cláusulas de função, lambdas e ramos de case.
def classify(n) when n > 0 -> "positive" def classify(0) -> "zero" def classify(n) -> "negative" classify(7) -- Output: "positive" classify(-3) -- Output: "negative" -- Guards in a case expression def size(n) -> case n is x when x > 100 = "big" | x when x > 10 = "medium" | _ = "small" end size(50) -- Output: "medium" -- Guards on a lambda check_pos = \x when x > 0 -> "yes" check_pos(4) -- Output: "yes"
Inteiros e doubles negativos podem ser casados diretamente como padrões literais:
def sign(-1) -> "minus one" def sign(_) -> "other" sign(-1) -- Output: "minus one"
Quando o mesmo nome de variável aparece mais de uma vez em um padrão, os valores casados devem ser iguais para a cláusula se aplicar:
def same(x, x) -> "equal" def same(_, _) -> "different" same(3, 3) -- Output: "equal" same(3, 4) -- Output: "different"
def isFive(5) também casa com 5.0.
Chamar uma função com menos argumentos do que ela espera retorna uma aplicação parcial que aguarda o restante. Desative por arquivo com o pragma no-curry.
def add(x, y) -> x + y add2 = add(2) add2(10) -- Output: 12 add(2)(40) -- Output: 42
Uma função cujas cláusulas não cobrem todas as entradas possíveis é reportada no carregamento do arquivo, para que funções parciais não falhem silenciosamente em tempo de execução. Adicione a cláusula faltante ou desative por arquivo com o pragma partial.
def head([h|_]) -> h
-- Error: function 'head' is not exhaustive (no clause for [])
def head([h|_]) -> h
def head([]) -> error("head: empty list")
-- OK
O operador )| encaminha um valor para uma função como seu primeiro argumento, permitindo ler transformações de dados da esquerda para a direita. x )| f é o mesmo que f(x), e x )| f(a, b) é o mesmo que f(x, a, b).
[3, 1, 2] )| reverse -- Output: [2, 1, 3] (same as reverse([3, 1, 2])) [1, 2, 3, 4] )| filter(\x -> x > 2) -- Output: [3, 4] (same as filter([1, 2, 3, 4], \x -> x > 2))
O operador é associativo à esquerda, então os pipes se encadeiam em um pipeline:
[1, 2, 3, 4] )| filter(\x -> x % 2 == 0) )| map(\x -> x * 10) -- Output: [20, 40]
O pipe tem precedência mais baixa que a aritmética, e você pode encaminhar diretamente para uma lambda:
2 + 3 )| (\n -> n == 5) -- Output: true (2 + 3 is evaluated first)
map e filter, onde a coleção encaminhada naturalmente se torna o primeiro argumento.
Você pode definir seus próprios operadores infixos - tanto simbólicos como ^ quanto de palavra como dot. Um operador personalizado é apenas uma função de dois argumentos cujo nome é um símbolo ou uma palavra, então casamento de padrões, guardas, currying e uso como função comum funcionam exatamente como para qualquer outra função.
Escreva a definição do jeito que você vai chamar - os operandos de cada lado do operador, entre parênteses depois de def:
def (x ^ 0) -> 1 def (x ^ y) -> x * (x ^ (y - 1)) 2 ^ 10 -- Saída: 1024
Cada def (lhs op rhs) é uma cláusula, então você tem múltiplas cláusulas, padrões literais e guardas when como em uma função nomeada:
def (n ~? 0) -> "zero" def (n ~? m) when m > 0 -> "positive" def (n ~? m) -> "negative" 5 ~? (-3) -- Saída: "negative"
Quando você escreve uma expressão sem parênteses, duas coisas decidem como ela agrupa. Você as define com uma linha infix opcional, escrita em inglês claro:
infix ^ groups right, binds tighter than *
Precedência ("binds tighter" / prende mais forte) é quem pega seus operandos primeiro - como * vem antes de + na matemática. Você posiciona seu operador em relação a um que já conhece:
binds tighter than <op> - precedência maior que <op>binds looser than <op> - precedência menor que <op>binds like <op> - aproximadamente igual a <op>A âncora <op> pode ser qualquer operador built-in (+ - * / % == < > && || …) ou outro operador que você definiu acima.
Associatividade ("groups" / agrupa) decide qual lado agrupa primeiro quando o mesmo operador se repete:
| Você escreve | groups left → | groups right → |
|---|---|---|
a ~ b ~ c | (a ~ b) ~ c | a ~ (b ~ c) |
groups left - o que a maioria dos operadores quer (como -: 10 - 3 - 2 é (10 - 3) - 2).groups right - o que a potência quer (2 ^ 2 ^ 3 é 2 ^ (2 ^ 3)).groups none - repeti-lo é um erro; você precisa parentizar (bom para comparações).infix inteira e um novo operador assume o padrão groups left, binds like *, então casos simples precisam só das cláusulas def.
Um operador simbólico vira um valor comum ao envolvê-lo em parênteses, estilo Haskell, e suporta seções:
(^)(2, 3) -- 8 (chamada prefixa) pow = (^) -- guarda o operador numa variável double = (^)(2) -- curried: \y -> 2 ^ y map([1, 2, 3], (^ 2)) -- [1, 4, 9] seção à direita: \x -> x ^ 2 map([1, 2, 3], (2 ^)) -- [2, 4, 8] seção à esquerda: \x -> 2 ^ x
Um operador de palavra não precisa de nada disso - seu nome já é um identificador, então funciona dos dois jeitos direto:
infix dot groups left, binds like * def (a dot b) -> sum(zipWith(a, b, \p, q -> p * q)) [1, 2, 3] dot [4, 5, 6] -- 32 (infixo) dot([1, 2, 3], [4, 5, 6]) -- 32 (prefixo - é só uma função)
Operadores se propagam pelo import exatamente como as funções que eles são. Defina um operador uma vez em uma biblioteca e todo arquivo que a importa pode usá-lo, infixo e tudo:
infix ^ groups right, binds tighter than * def (x ^ 0) -> 1 def (x ^ y) -> x * (x ^ (y - 1))
import mymath 2 ^ 10 -- Saída: 1024
! # $ % & * + - / : < = > ? @ ^ | ~ - que não seja já um operador built-in (+, *, ==, <|, …) ou um símbolo reservado. Então ^, <|>, >>= e <$> funcionam, e um operador personalizado nunca colide com um built-in. Operadores de palavra podem ser qualquer identificador que não seja já uma palavra-chave ou um operador de palavra built-in.
Bern permite importar bibliotecas para estender funcionalidades. Use a palavra-chave import para carregar bibliotecas padrão ou módulos personalizados.
import core
import core as my_core
Depois de importada, você pode usar funções como map, filter e conversões de tipo:
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,7,8,9,10], isEven) -- Output: [2,4,6,8,10]
No caso de apelidos, você deve especificar o apelido, seguido de dois-pontos e o nome desejado:
import core as c c:map([1,2,3], \x -> x * 2) -- Output: [2,4,6]
A biblioteca core fornece funções de conversão para tratar a entrada:
import core
age_text = input("Type your age: ")
age = to_int(age_text)
"Your age in 5 years: " + (age + 5)
Capture a entrada do usuário com a função input(), semelhante ao Python.
name = input("Type your name: ")
name
-- Displays whatever the user typed
Por padrão, input() retorna texto. Converta para outros tipos usando funções da biblioteca core:
import core
age_text = input("Type your age: ")
age = to_int(age_text)
"You'll be " + (age + 10) + " in 10 years"
input() é um ótimo primeiro arquivo para guardar. Salve seu código em um arquivo terminado em .brn e rode com bern meuarquivo.brn (ou carregue no REPL com :load meuarquivo.brn). Daqui o guia fica um pouco mais avançado - vá no seu ritmo, e mantenha o REPL por perto.
ADTs permitem definir tipos de dados personalizados com vários construtores, semelhantes a enums ou uniões etiquetadas em outras linguagens.
Use a palavra-chave adt seguida do nome do tipo e dos construtores:
adt Shape = Circle Double | Rectangle Double Double
Cada construtor pode ter zero ou mais campos de tipos específicos. Veja Tipos em Declarações de ADT para a lista completa de nomes de tipo de campo que você pode usar (Int, Double, Bool, Char, String, List, Set, Auto, ou outro ADT).
Uma declaração com muitos construtores pode ser quebrada em várias linhas, um construtor por linha, no estilo Haskell. Coloque o | no início de cada linha de continuação:
adt Expression = Num Double
| Bool Bool
| Add Expression Expression
| Sub Expression Expression
| If Expression Expression Expression
Isso é puramente cosmético - as formas de linha única e multi-linha são equivalentes, e você pode misturar os dois estilos livremente.
Chame construtores como funções para criar instâncias:
c = Circle(5.0) r = Rectangle(3.0, 4.0)
Desestruture ADTs nas definições de função para acessar seus campos:
def area(Circle(r)) -> 3.14159 * r * r def area(Rectangle(w, h)) -> w * h area(c) -- Output: 78.53975 (π * 5²) area(r) -- Output: 12.0 (3 * 4)
Você pode definir Tipos de Dados Algébricos como iteráveis com a palavra-chave iterative:
adt iterative Shape = Circle Double | Rectangle Double Double
Um padrão comum de ADT para representar valores opcionais:
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()
Por padrão, um erro em tempo de execução não derruba mais o programa. Em vez disso, ele vira um valor de erro de primeira classe que se propaga pelas expressões e pode ser inspecionado. O programa continua rodando.
import core bad = [1, 2, 3][99] -- out of bounds is_error(bad) -- Output: true "the program keeps running" -- Output: "the program keeps running"
Isso foi implementado para deixar Bern mais parecido com outras linguagens, como Go ou Odin. Dito isso, eu realmente acredito que você deveria usar Mônadas e ADTs para tratamento de erros!
Cria um valor de erro recuperável.
Verifica se um valor é um erro.
def safe_head([]) -> error("empty list")
def safe_head([h|_]) -> h
result = safe_head([])
if is_error(result) then
"no head"
else
result
end
abort-on-error para restaurar o comportamento clássico de falhar em erros.
Bern oferece funções embutidas para operações de arquivo e utilitários de sistema.
read_file(path) - Lê o conteúdo de um arquivo como stringwrite_file(path, content) - Escreve conteúdo em um arquivoget_current_dir() - Obtém o diretório de trabalho atualcontent = read_file("myfile.txt")
write_file("output.txt", "Hello, Bern!")
write_file("data.txt", content + "\nNew line")
Retorna o sistema em que o usuário está. Útil para bindings.
os = get_host_machine() -- Returns OS/system information
Aplica uma função a cada elemento de uma coleção. Usada também em Tipos de Dados Algébricos (ADTs) para modificar valores contidos sem quebrar o contexto functorial.
ast Maybe = Just Int | None fmap(Just(5), \x -> x * 2) -- Output: Just(10)
Retorna a lista das chaves de um objeto, na ordem de inserção. Útil para iterar ou serializar objetos.
obj = #{ name: "Bern", version: 3 }#
keys(obj)
-- Output: ["name", "version"]
O Bern vem com uma biblioteca padrão completa e módulos vendor embutidos. Cada biblioteca agora tem sua própria página - com referência completa das funções e exemplos práticos, em inglês e português.
As bibliotecas padrão são importadas pelo nome (import math); os módulos vendor usam o caminho (import vendor/csv).
Você pode chamar funções C diretamente de Bern usando a interface de função estrangeira (FFI). Isso permite aproveitar bibliotecas C existentes e chamadas de sistema.
Para isso, você precisa declarar a assinatura da função C usando a palavra-chave foreign, especificando a biblioteca, o nome da função, os tipos dos argumentos e o tipo de retorno.
foreign example("lib.so", "int") -> "void"
Recomendação: use get_host_machine() dentro de uma função auxiliar para obter o caminho correto da biblioteca em vários sistemas operacionais.
def get_path() do
os = get_host_machine()
if os == "mingw64" || os == "mingw32" then
return "windows_lib.dll"
else
return "linux_lib.so.6"
end
end
O comando build empacota um script em um único executável autocontido. Ele examina as instruções import do script (transitivamente) e embute esses arquivos de biblioteca junto ao script, então o programa resultante roda mesmo onde não há a pasta lib/.
bern build app.brn -- Creates app.exe bern build app.brn -o myprogram.exe -- Creates myprogram.exe
O executável empacotado roda o script embutido diretamente:
./app.exe -- Runs app.brn with its bundled libraries
main.brn embutido de lá. O seu diretório de trabalho permanece intocado.
Pragmas são diretivas de nível de arquivo que ajustam o comportamento de Bern. Escreva-as como um comentário mágico, normalmente no topo do arquivo:
{--! strict-types !--}
{--! immutable !--}
A tabela abaixo é a referência rápida. Para uma explicação, um exemplo e o caso de uso de cada pragma, veja a → página de Pragmas.
| Pragma | Efeito |
|---|---|
impure-lists | listas podem conter tipos de elementos mistos |
impure-sets | operações de conjunto mantêm duplicatas |
strict-types | proíbe a coerção implícita entre string e número |
strict-arithmetic | divisão por zero é um erro, não NaN |
immutable | reatribuir uma variável existente é um erro |
no-eval | expressões soltas não imprimem automaticamente (print() ainda funciona) |
show-types | valores impressos automaticamente mostram seu tipo |
safe-index | indexação fora dos limites retorna undefined |
no-undefined | ler uma variável não vinculada é um erro |
start-on-one | a indexação começa em 1 |
main | executa main() após o carregamento do arquivo |
no-curry | desativa o currying automático |
abort-on-error | falha em erros de execução em vez de produzir valores de erro |
partial | permite funções não exaustivas |
no-written-operators | desliga os operadores em palavras, liberando nomes como length e plus para uso como variáveis |
typed | Bern 2.1 ativa declarações de variável com tipo, nome :: Tipo = valor, verificadas em tempo de execução (veja Declarações de Variável Tipadas) |