Aprenda Bern

Instale, abra o REPL e acompanhe - da sua primeira expressão até os seus próprios programas.

Instalando o Bern

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.

Windows (PowerShell)

iex (irm https://bern-lang.github.io/Bern/install/install_bern.ps1)

Linux & macOS (Bash)

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.

Sem compromisso: o Bern é um único programa autocontido. Se um dia quiser removê-lo, é só apagar - não há mudanças no sistema para desfazer.

O REPL

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.

A Variável _

O resultado da última expressão é vinculado a _, então você pode construir a partir dele:

[bern]: 1 + 2
3
[bern]: _ * 10
30

Blocos de Várias Linhas

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

Comandos do REPL

ComandoAção
:help, :hMostra a lista de comandos
:load <file>, :lCarrega e executa um arquivo .brn na sessão atual
:resetLimpa as definições locais
:quit, :qSai (ou pressione Ctrl-D)

O histórico de comandos está disponível com as setas para cima/baixo e é salvo entre sessões.

Básico

É 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.

Literais

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

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

Aritmética

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.

Avaliação

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

Verificação de Tipo e Comprimento

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 string
typeof_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.

Tipos

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.

Tipos Embutidos

Todo valor em Bern é um dos seguintes. A coluna nome no typeof é a string exata que :: retorna para aquele valor.

Tiponome no typeofExemploDescrição
Integer"Integer"2, -42, 0Números inteiros.
Double"Double"3.14, -2.5, 1.0Números de ponto flutuante.
Boolean"Boolean"true, falseValores 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 + yUma função nomeada definida com def.
Lambda"Lambda"\x -> x + 1Uma 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 definidaA 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.
ADTo próprio nomeCircle(5.0)Um valor definido pelo usuário. Reporta o próprio nome do tipo, ex.: "Shape". Veja ADTs.

Inspecionando o Tipo de um Valor

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"
Nota: Uma string é uma lista de caracteres, então :: "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.
Procurando os tipos que você pode usar dentro de um ADT? Clique aqui! Bern 2.0

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 adtReportado por :: comoSignificado
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 / Anyo tipo do próprio valorBern 2.1 Campo dinâmico: aceita um valor de qualquer tipo, detectado automaticamente. Ideal para uma linguagem de tipagem dinâmica.
qualquer nomeesse nomeOutro 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.

Quer variáveis com tipo verificado pelo pragma typed? Clique aqui! Bern 2.1

Com 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

Operadores

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.

Operadores em Palavras

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ímboloPalavraSímboloPalavra
+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"
Nota: as palavras usam hifens (is-greater-or-equal) e se resolvem da mesma forma que seus símbolos, com a mesma precedência.

Escopo

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.

Escopo Local (=)

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.

Escopo Global (:=)

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 vinculaApenas no escopo atualTabela global de todo o programa
Visível dentro de outras funçõesNãoSim
Sobrevive após o fim do escopoNãoSim
Uso típicoValores temporários / de trabalhoEstado compartilhado e duradouro

Como um Nome É Resolvido

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)
Nota: Como os globais vencem na resolução, evite reusar o nome de um global para um valor local sem relação. Escolha nomes distintos, ou atualize o próprio global com := se você realmente quiser mudar o valor compartilhado.

Condicionais

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.

Instrução If-Then-Else

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."

Cadeias Else-If

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."

Exemplo: FizzBuzz

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

Avaliação em Curto-Circuito Bern 2.0

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)

Laços

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.

Nota: for é aceito em todos os lugares onde loop é, como um sinônimo retrocompatível. loop é a grafia preferida.

Laço de Repetição

Executa um bloco um número específico de vezes:

loop 3 do
    "Hello, Loops!"
end
-- Output: "Hello, Loops!" (three times)

Laço Condicional

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)

Laço Infinito

Crie um laço infinito com loop true:

loop true do
    "This runs forever!"
end
Nota: cuidado com laços infinitos! Garanta que você tenha uma forma de sair deles em seus programas reais.

Laço Loop-In

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"

Loop-In com Índice

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"

Listas

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.

Declarando Listas

numbers_list = [1,2,3,4,5]
numbers_list
-- Output: [1,2,3,4,5]

Sintaxe de Intervalo

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]

Indexação

Acesse elementos específicos usando indexação baseada em zero:

range_list = [5..10]
range_list[3]
-- Output: 8 (fourth element)

Aritmética de Listas

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]

Operações de Conjunto em Listas

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]

Operações Complexas com Listas

result = ([1,2,3,4] <> [3..5]) <| ([2,3,4] |> [4,5])  [1]
-- Combines concatenation, symmetric difference,
-- intersection, and regular difference

Indexação Negativa Bern 2.0

Índices negativos contam a partir do fim de uma lista ou string.

[10, 20, 30][-1]
-- Output: 30
"Bern"[-1]
-- Output: 'n'

Intervalos Preguiçosos Bern 2.0

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]

Desestruturando Listas (Padrões Cons)

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.

Nota: a mesmíssima sintaxe [head | tail] também funciona como expressão - veja Expressões Cons abaixo.

Expressões Cons Bern 2.0

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.

List Comprehensions Bern 2.0

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']
Nota: uma comprehension precisa de pelo menos um gerador. Sem um <-, [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).

Conjuntos

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.

Declarando Conjuntos

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

Indexação

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)

Aritmética de Conjuntos

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 de Conjunto

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}
Lembre-se: conjuntos removem duplicatas automaticamente e não garantem a ordem. Se você precisa de dados ordenados ou de duplicatas, use listas.

Objetos

Bern suporta objetos (hashmaps/hashtables) usando a notação #{...}# para pares chave-valor.

Declarando Objetos

obj = #{
    key: "value",
    hello: "world"
}#
obj
-- Output: #{key: "value", hello: "world"}#

Atualizando Valores

Reatribua valores usando a notação de colchetes:

obj["key"] = "new_value"
obj
-- Output: #{key: "new_value", hello: "world"}#

Adicionando Campos

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 Aninhados

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"

Exemplo Completo de Objeto

person = #{
    name: "Alice",
    age: 25,
    address: #{
        city: "Boston",
        zip: "02101"
    }#
}#

person["address"]["city"]
-- Output: "Boston"

Funções

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.

Definição Simples de Função

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

Casamento de Padrões

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"

Curingas e Casamento de Literais

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!"

Funções com Corpo em Bloco

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 Lambda

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 de Ordem Superior

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)

Guardas (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"

Padrões Literais Negativos

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"

Variáveis Repetidas em Padrões

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"
Nota: padrões numéricos casam entre inteiro e double: def isFive(5) também casa com 5.0.

Currying Automático Bern 2.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

Casamento Exaustivo Bern 2.0

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

Operador Pipe

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))

Encadeamento

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]

Precedência e Lambdas

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)
Dica: pipelines funcionam melhor com funções que recebem a coleção primeiro, como map e filter, onde a coleção encaminhada naturalmente se torna o primeiro argumento.

Operadores Personalizados Bern 2.1

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.

Definindo um operador

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"

Fixidez: precedência e associatividade

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ê escrevegroups leftgroups right
a ~ b ~ c(a ~ b) ~ ca ~ (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).
As duas cláusulas são opcionais. Pule a linha infix inteira e um novo operador assume o padrão groups left, binds like *, então casos simples precisam só das cláusulas def.

Operadores como funções comuns

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)

Compartilhando operadores entre arquivos

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:

lib/mymath.brn

infix ^ groups right, binds tighter than *
def (x ^ 0) -> 1
def (x ^ y) -> x * (x ^ (y - 1))

main.brn

import mymath
2 ^ 10
-- Saída: 1024
Nomes de operadores simbólicos são qualquer sequência de caracteres de operador - ! # $ % & * + - / : < = > ? @ ^ | ~ - 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.

Importações

Bern permite importar bibliotecas para estender funcionalidades. Use a palavra-chave import para carregar bibliotecas padrão ou módulos personalizados.

Importando a Biblioteca Core

import core

Importando com um Apelido

import core as my_core

Funções da Biblioteca 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]

Conversão de Tipo

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)

Entrada do Usuário

Capture a entrada do usuário com a função input(), semelhante ao Python.

Entrada Básica

name = input("Type your name: ")
name
-- Displays whatever the user typed

Convertendo a Entrada

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"
Pronto para mais? Você já sabe o suficiente para escrever programas de verdade - e um que pede 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.

Tipos de Dados Algébricos (ADTs)

ADTs permitem definir tipos de dados personalizados com vários construtores, semelhantes a enums ou uniões etiquetadas em outras linguagens.

Definindo ADTs

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).

Declarações Multi-linha Bern 2.1

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.

Criando Instâncias

Chame construtores como funções para criar instâncias:

c = Circle(5.0)
r = Rectangle(3.0, 4.0)

Casamento de Padrões com ADTs

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)

Tipos de Dados Iterativos

Você pode definir Tipos de Dados Algébricos como iteráveis com a palavra-chave iterative:

adt iterative Shape = Circle Double | Rectangle Double Double

O Tipo Maybe

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()
Caso de uso: o tipo Maybe é perfeito para operações que podem falhar, permitindo tratar os casos de sucesso e falha explicitamente, sem exceções.

Erros como Valores Bern 2.0

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!

Lançando e testando erros

error(message) → error value

Cria um valor de erro recuperável.

is_error(value) → boolean

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
Nota: use o pragma abort-on-error para restaurar o comportamento clássico de falhar em erros.

Funções Globais

Bern oferece funções embutidas para operações de arquivo e utilitários de sistema.

Operações de Arquivo

  • read_file(path) - Lê o conteúdo de um arquivo como string
  • write_file(path, content) - Escreve conteúdo em um arquivo
  • get_current_dir() - Obtém o diretório de trabalho atual
content = read_file("myfile.txt")
write_file("output.txt", "Hello, Bern!")
write_file("data.txt", content + "\nNew line")

Informações do Sistema

Nota: disponível apenas nas versões 0.1.2 e superiores do Bern.

Retorna o sistema em que o usuário está. Útil para bindings.

os = get_host_machine()
-- Returns OS/system information

fmap

Nota: disponível apenas nas versões 0.1.2 e superiores do Bern.

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)

keys

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"]

Referência da Biblioteca Padrão

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.

→ Ver todas as bibliotecas

As bibliotecas padrão são importadas pelo nome (import math); os módulos vendor usam o caminho (import vendor/csv).

Biblioteca padrão

  • core - operações de lista, folds, auxiliares de ordem superior, conversões
  • math - constantes, potências, raízes, logaritmos, trigonometria
  • strings - fatiar, dividir, juntar, caixa, busca
  • random - números, escolhas e strings aleatórias
  • assert - auxiliares de asserção para testes
  • path - juntar, normalizar e inspecionar caminhos
  • url - codificação, query strings, parsing/construção de URL
  • crypto - tokens, checksums, IDs estilo UUID
  • json - parse e serialização de JSON

Módulos vendor

Bindings em C

Nota: disponível apenas nas versões 0.1.2 e superiores do Bern.

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

Empacotando para .exe

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
Como funciona: o script e suas bibliotecas são anexados a uma cópia do interpretador como um blob. Na inicialização, o interpretador detecta o blob, o extrai para um diretório temporário e roda o main.brn embutido de lá. O seu diretório de trabalho permanece intocado.

Pragmas Bern 2.0

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.

PragmaEfeito
impure-listslistas podem conter tipos de elementos mistos
impure-setsoperações de conjunto mantêm duplicatas
strict-typesproíbe a coerção implícita entre string e número
strict-arithmeticdivisão por zero é um erro, não NaN
immutablereatribuir uma variável existente é um erro
no-evalexpressões soltas não imprimem automaticamente (print() ainda funciona)
show-typesvalores impressos automaticamente mostram seu tipo
safe-indexindexação fora dos limites retorna undefined
no-undefinedler uma variável não vinculada é um erro
start-on-onea indexação começa em 1
mainexecuta main() após o carregamento do arquivo
no-currydesativa o currying automático
abort-on-errorfalha em erros de execução em vez de produzir valores de erro
partialpermite funções não exaustivas
no-written-operatorsdesliga os operadores em palavras, liberando nomes como length e plus para uso como variáveis
typedBern 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)