Bern por Exemplos

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.

Como ler esta página: os trechos curtos mostram uma ideia de cada vez, com a saída esperada escrita como um comentário (-- 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.

Introdução

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:

  1. Avaliação implica saída. Qualquer expressão que você escreve no nível superior é avaliada e impressa automaticamente - não há a cerimônia do print para experimentos rápidos.
  2. Tudo é uma expressão. if, case e corpos de função produzem um valor, então podem ser atribuídos, retornados ou encaminhados por pipe.
  3. Símbolos e palavras são intercambiáveis. a + b e a plus b são o mesmo programa. Iniciantes podem escrever palavras; quando os símbolos ficarem naturais, troque à vontade.

Literais & Saída Automática

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
Nota: um único caractere entre aspas simples ('n') é um caractere, enquanto aspas duplas ("n") criam uma string de um caractere. Indexar uma string retorna caracteres.

Variáveis & Tipos

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

Verificação de Tipo & Comprimento

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

Aritmética, Comparação & Lógica

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'

Operadores em Palavras

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ímboloPalavraSímboloPalavra
+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 ->.

Condicionais

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ão

Como 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

Laços

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

Laço de repetição

Dê a ele um número para repetir um corpo essa quantidade de vezes.

loop 3 do
    "Hello, Loops!"
end

Laço condicional

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

Laço infinito

loop true repete para sempre - útil para servidores e programas no estilo REPL (interrompa com break ou saindo).

loop true do
    "Hello!"
end

Laço loop-in

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

Loop-in com índice

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

Definindo Funções

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

Casamento de Padrões

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

Guardas

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"

Lambdas & Funções de Ordem Superior

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

Currying & Aplicação Parcial

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]

O Operador Pipe

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]
Precedência: o pipe tem precedência mais baixa que a aritmética, então em 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 & Intervalos

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)
OperadorPalavraSignificado
<>concatJunta duas coleções ponta a ponta
<|unionTodos os elementos de ambas, sem duplicatas
|>intersectSomente os elementos presentes em ambas
</>differenceElementos na primeira, mas não na segunda

Conjuntos

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

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"

Tipos de Dados Algébricos

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

O padrão Maybe

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

Expressões case

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

JSON

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
Nota: um array JSON é mapeado para um Conjunto Bern. Um valor que se repete colapsa em uma sub-lista de suas cópias, então a teoria dos conjuntos permanece intacta enquanto a multiplicidade é preservada - por exemplo, [1, "a", 34.2, "a"] é convertido em {1, ["a", "a"], 34.2}, e retorna ao array original na ida e volta.

Erros como Valores

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 Preguiçosos

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]

Importações

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.

Entrada do Usuário

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)