As novidades de cada versão da linguagem Bern.
Bern 2.1 constrói sobre a base do 2.0: operadores definidos pelo usuário, anotações de tipo opcionais, declarações multi-linha mais amigáveis, uma correção no lexer para identificadores que começam com uma palavra-chave, e um conjunto de combinadores de parser bem maior. A referência de bibliotecas do site também foi reconstruída em torno de uma página por biblioteca.
Operadores definidos pelo usuário. Agora você pode declarar seus próprios operadores infixos - simbólicos como ^ e de palavra como dot - com def (x ^ y) -> .... Por baixo dos panos são funções comuns de dois argumentos, então casamento de padrões, guardas e currying simplesmente funcionam, e um operador simbólico pode ser usado como valor puro ((^)) ou como seção ((^ 2), (2 ^)). Uma linha infix opcional, em inglês claro, define a fixidez - infix ^ groups right, binds tighter than * - e operadores se propagam pelo import como as funções, então dá para defini-los uma vez em uma biblioteca.
infix ^ groups right, binds tighter than * def (x ^ 0) -> 1 def (x ^ y) -> x * (x ^ (y - 1)) 2 ^ 10 -- 1024 2 * 3 ^ 2 -- 18 (^ prende mais forte que *) map([1, 2, 3], (^ 2)) -- [1, 4, 9] (uma seção)
Declarações de variável tipadas. Com o novo pragma typed, uma atribuição pode carregar um tipo: nome :: Tipo = valor. O valor é verificado contra o tipo declarado quando a atribuição executa, e uma incompatibilidade é um erro fatal, então um arquivo tipado falha alto em vez de seguir errado. É totalmente opcional - sem o pragma, a sintaxe fica inativa e :: mantém seu significado usual de operador typeof.
{--! typed !--} count :: Integer = 10 name :: String = "bern" ratio :: Double = 3.14 -- count :: Integer = "oops" -- erro: incompatibilidade de tipo
O tipo declarado aceita os nomes canônicos do typeof (Integer, Double, Boolean, Character, String, List, Set, Object), os apelidos familiares Int, Float, Char, Bool, Text, e qualquer nome de ADT (que casa com um valor construído por qualquer um dos construtores daquele tipo).
O tipo Auto / Any. Um tipo dinâmico que aceita um valor de qualquer tipo - a forma explícita de dizer "qualquer coisa serve" numa linguagem de tipagem dinâmica. Funciona tanto como anotação de variável tipada quanto como tipo de campo de ADT.
{--! typed !--} anything :: Auto = 7 anything :: Auto = "agora uma string" -- Auto nunca reclama adt Box = Box Auto -- um campo que guarda qualquer valor
Declarações de tipo multi-linha. Um tipo com muitos construtores pode ser dividido em várias linhas, um construtor por linha, com o | no início de cada linha de continuação - estilo Haskell. É puramente cosmético; as formas de uma linha e multi-linha são equivalentes.
adt Expression = Num Double | Add Expression Expression | Sub Expression Expression | If Expression Expression Expression
Identificadores com prefixo de palavra-chave. Nomes que apenas começam com uma palavra reservada agora são tokenizados como um único identificador, em vez da palavra-chave mais o texto restante. Variáveis, funções, parâmetros e nomes de ADT como truevar, island, ending, define ou importance finalmente se comportam como esperado.
island = 3 -- um identificador, não `is` + `land` def define(x) -> x + 1 truevar = true -- o literal ainda parseia onde um valor é esperado
Apelidos amigáveis do BernParsec. A biblioteca de combinadores de parser vendor/bernparsec agora traz um conjunto completo de nomes em inglês claro que encaminham para o núcleo estilo megaparsec: literalChar, text, charIn, digit, letter, zeroOrMore, oneOrMore, separatedBy, keepLeft/keepRight, firstOf, orTry, integerNumber, decimalNumber, inParens, peek, deferred, chainLeft, run, quickParse, prettyError, e mais. Misture os dois estilos livremente.
Operadores do BernParsec. Agora ele também traz operadores infixos estilo megaparsec - <$> (map), <*> (applicative), <* / *> (mantém esquerda / direita), <|> (escolha), >>= (bind) e <?> (rótulo) - construídos sobre os novos operadores definidos pelo usuário, então uma gramática se lê quase como a coisa que descreve: char('$') *> num, ou string("if") <|> string("else").
import vendor/bernparsec quickParse(separatedBy(digits(), literalChar(',')), "1,2,33") -- [1, 2, 33]
BernParsec como dado (forma ADT). Parsers, resultados e valores opcionais também podem ser expressos como dados pelo ADT BPParser (os construtores BP*), com resultados BPOk/BPErr e opcionais BPJust/BPNothing. Rode um parser pelo caminho ADT com parseADT / runParserADT quando quiser um resultado sobre o qual fazer pattern matching.
Uma página por biblioteca. A biblioteca padrão e os módulos vendor embutidos têm cada um sua própria página de referência agora - em inglês e português - com toda função documentada por exemplo e uma seção "juntando tudo" que as combina. Um novo hub de Bibliotecas liga todas elas.
Leitura mais limpa. Breadcrumbs foram adicionados pela documentação, e o site inteiro passou por uma revisão mais minimalista: regras mais finas, cabeçalhos de tabela neutros e acentos mais suaves.
Bern 2.0 é uma versão importante. Ela transforma o interpretador em uma linguagem configurável: um conjunto de pragmas permite optar por comportamentos mais rígidos ou mais flexíveis por arquivo, e várias arestas antigas agora têm padrões sensatos. Também adiciona JSON, operadores em palavras, currying, intervalos preguiçosos e erros recuperáveis.
Erros como valores. Um erro em tempo de execução não derruba mais o programa inteiro. Em vez disso, ele vira um valor de primeira classe que você pode guardar, repassar e inspecionar. Indexação fora dos limites, buscas que falham e descuidos parecidos produzem um valor de erro que se propaga silenciosamente pelo resto da expressão, então o programa continua rodando. Teste qualquer valor com is_error e lance o seu próprio com error. Se preferir o antigo comportamento de falhar, o pragma abort-on-error o traz de volta.
import core bad = [1, 2, 3][99] -- fora dos limites, mas sem falha is_error(bad) -- true "the program keeps running" -- esta linha ainda é impressa def safe_head([]) -> error("empty list") def safe_head([h|_]) -> h is_error(safe_head([])) -- true safe_head([10, 20]) -- 10
Currying automático. Chamar uma função com menos argumentos do que ela declara não falha mais. O Bern retorna uma nova função que lembra os argumentos já fornecidos e espera pelos demais, o que torna naturais a aplicação parcial e os pipelines point-free. Desative por arquivo com o pragma no-curry.
def add(x, y) -> x + y add2 = add(2) -- uma aplicação parcial, esperando mais um argumento add2(10) -- 12 add(2)(40) -- 42 (a mesma chamada, em uma única linha)
Intervalos preguiçosos. Intervalos agora são preguiçosos. Escrever um intervalo não aloca seus elementos, então mesmo um intervalo astronomicamente grande é criado instantaneamente. Seu comprimento é conhecido em tempo O(1), e os elementos só são calculados quando algo realmente os pede.
big = [1..1000000000] -- criado instantaneamente, nada materializado ainda :> big -- 1000000000 (comprimento em O(1)) take(5, big) -- [1, 2, 3, 4, 5] (só estes cinco são materializados)
Casamento exaustivo. Quando uma função é definida por várias cláusulas, o Bern agora verifica no carregamento se essas cláusulas cobrem todas as entradas possíveis. Uma função que poderia ser chamada com um argumento que nenhuma cláusula casa é reportada de antemão, antes de rodar. A verificação entende listas, booleanos, conjuntos e tipos de dados algébricos - um casamento que cobre todos os construtores de um ADT conta como exaustivo, do mesmo jeito que em Haskell. Se uma função parcial for intencional, desative para o arquivo todo com o pragma partial.
-- reportado no carregamento: esta cláusula não casa toda entrada def describe(0) -> "zero" -- declare a intenção explicitamente para silenciar a verificação {--! partial !--} def describe(0) -> "zero"
Lógica de curto-circuito. Os operadores && e || agora fazem curto-circuito. O lado direito só é avaliado quando ainda pode mudar o resultado, então uma proteção em linha contra algo como divisão por zero é segura de escrever.
false && (1 / 0 == 0) -- false, e o lado direito arriscado nunca executa true || (1 / 0 == 0) -- true, avaliado da mesma forma preguiçosa
Indexação negativa. A indexação aceita posições negativas que contam de trás para frente a partir do fim de uma lista, então você alcança o último elemento sem antes medir o comprimento.
[10, 20, 30][-1] -- 30 (o último elemento) [10, 20, 30][-2] -- 20 (o penúltimo)
Nomes reservados são protegidos. Atribuir a uma palavra-chave ou a um operador em palavras (length, plus, …) agora falha com uma mensagem clara, em vez de criar silenciosamente uma variável que você nunca conseguiria ler de volta. Para usar um dos nomes de operador como variável, desligue os operadores em palavras com o pragma no-written-operators.
length = 10 -- erro: cannot assign to 'length': it is a written-word operator {--! no-written-operators !--} length = 10 -- agora tudo bem: 'length' é uma variável comum
Diretivas de nível de arquivo. Um pragma é um comentário mágico, normalmente no topo do arquivo, que faz aquele arquivo optar por um comportamento mais rígido ou mais flexível. Vários podem ser combinados, e eles só afetam o arquivo em que aparecem.
{--! strict-types !--} {--! immutable !--} x = "count: " + 5 -- erro: strict-types proíbe a coerção implícita x = 10 x = 11 -- erro: immutable proíbe a reatribuição
| 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 |
Operadores em palavras. Todo operador simbólico agora tem uma grafia em português corrente, então você pode escrever código que se lê em voz alta como uma frase: plus, minus, times, divided-by, is-greater-or-equal, and, or, not, concat, and-do (pipe), be (atribuição) e muito mais. Símbolos e palavras são intercambiáveis e podem ser misturados livremente.
3 is-greater 2 and 5 is-less-or-equal 5 -- true [1, 2] concat [3, 4] -- [1, 2, 3, 4] total be 10 plus 5 -- atribuição: total agora é 15
O operador pipe )|. O pipe encaminha um valor para uma função como seu primeiro argumento, então uma sequência de transformações se lê da esquerda para a direita, na ordem em que acontece, em vez de aninhada de dentro para fora.
[1, 2, 3, 4] )| filter(\x -> x % 2 == 0) -- [2, 4]
Sintaxe de função legível. Corpos de função e lambda podem ser introduzidos com as palavras returns e such-that no lugar da seta ->, e uma lambda pode ser escrita com a palavra-chave lambda. As formas simbólicas continuam funcionando em todo lugar.
def double(n) returns n * 2 -- 'returns' se lê melhor que -> double(21) -- 42 map([1, 2, 3], lambda x such-that x * 10) -- [10, 20, 30]
Guardas de casamento de padrões. Uma cláusula de função pode carregar uma guarda when. A cláusula só casa quando seus padrões batem e sua expressão de guarda é verdadeira, deixando você ramificar por valor, não apenas pela forma.
def grade(n) when n is-greater 90 -> "A" def grade(n) when n is-greater 70 -> "B" def grade(n) -> "C" grade(95) -- "A"
A palavra-chave for-in. Os laços aceitam a palavra in ao lado do separador :, então iterar sobre uma coleção se lê como português comum.
loop char in "Bern" do char end
A palavra-chave loop. loop agora é a grafia preferida para todas as formas de laço - repetição, condicional e iteração. A palavra-chave for original continua funcionando em todos os lugares como um sinônimo retrocompatível.
loop 3 do "hi" end -- repetição loop n : [1, 2, 3] do n end -- iteração (for ... ainda funciona)
List comprehensions. Uma lista pode ser construída a partir de um ou mais geradores (padrão <- fonte) e guardas booleanas opcionais, escrita como [expressão | qualificadores]. Vários geradores aninham como laços para produzir todas as combinações, o padrão de um gerador desestrutura enquanto itera (elementos que não casam são ignorados) e as guardas filtram o resultado.
[x * x | x <- [1..5]] -- [1, 4, 9, 16, 25] [x | x <- [1..10], x % 2 == 0] -- [2, 4, 6, 8, 10] [a + b | a <- [1, 2], b <- [10, 20]] -- [11, 21, 12, 22]
Expressões cons. A sintaxe [head | tail] agora também funciona como expressão, adicionando um ou mais elementos no início de uma lista existente (e {head | tail} em um conjunto). A adição no início é preguiçosa, então você pode fazer cons sobre um intervalo enorme e ainda obter um resultado instantâneo.
[0 | [1, 2, 3]] -- [0, 1, 2, 3] [1, 2 | [3, 4]] -- [1, 2, 3, 4] (vários elementos no início) big = [0 | [1..1000000000]] -- instantâneo; :> big é 1000000001
Leitura e escrita de JSON. A nova biblioteca json lê e escreve texto JSON. json_parse transforma uma string JSON em valores Bern que você pode indexar, e json_stringify os serializa de volta.
import json person = json_parse("{\"name\": \"Bern\", \"version\": 2}") person["name"] -- "Bern" json_stringify(person) -- {"name":"Bern","version":2}
Arrays JSON baseados em conjunto. Arrays JSON mapeiam para o modelo baseado em conjunto do Bern de um jeito que preserva a multiplicidade dos elementos duplicados, agrupando valores repetidos em vez de descartá-los.
import json json_parse("[1, \"a\", 34.2, \"a\"]") -- {1, ["a", "a"], 34.2} (duplicatas agrupadas, multiplicidade preservada)
keys(object). O embutido keys retorna a lista de chaves de um objeto, o que facilita percorrer um hashmap cuja forma você não conhece de antemão.
obj = #{ name: "Bern", version: 2 }# keys(obj) -- ["name", "version"]
error e is_error. Dois embutidos sustentam o novo modelo de valores de erro: error constrói um valor de erro que carrega uma mensagem, e is_error informa se um dado valor é um deles.
boom = error("something went wrong") is_error(boom) -- true is_error(42) -- false
REPL aprimorado. O REPL interativo ganhou histórico persistente entre sessões, blocos de várias linhas, o último resultado vinculado a _ e os metacomandos :help, :load, :reset e :quit. Pragmas podem ser ativados a partir do prompt com :pragma <nome> (e listados com :pragmas), além dos comentários {--! … !--} em linha. Um erro de execução agora é impresso e devolve você ao prompt em vez de encerrar a sessão.
> x = 21 > x * 2 42 > _ + 8 -- _ é o resultado anterior 50 > :help -- lista os metacomandos
Executáveis independentes. bern build empacota um script junto com cada biblioteca que ele importa em um único executável independente, que roda sem uma instalação separada do Bern.
bern build app.brn -o myprogram.exe
./myprogram.exe -- executa app.brn com suas bibliotecas empacotadas
A linha 1.x final focou em casamento de padrões mais rico, um sistema de módulos mais limpo e ferramentas de editor.
Condicionais como expressões. Um if/then/else agora é uma expressão que avalia para um valor, e pode ser encadeado em escadas else-if. Como retorna um valor, ele pode aparecer em qualquer lugar onde uma expressão é esperada, inclusive diretamente como corpo de uma função.
def sign(n) -> if n > 0 then "positive" else if n < 0 then "negative" else "zero" end sign(-3) -- "negative"
A expressão case ... is. Essa expressão casa um valor contra uma série de padrões em linha e retorna o ramo que casou. Ela pode casar por construtor e vincular seus campos, e com ::v pode casar pelo nome do tipo do valor como uma string.
adt Shape = Circle Double | Square Double def name(v) -> case v is Circle(_) = "circle" | Square(_) = "square" | _ = "?" end name(Circle(5.0)) -- "circle" def kind(v) -> case ::v is "Circle" = "a circle" | _ = "something else" end
ADTs iteráveis. Um ADT declarado com a palavra-chave iterative pode ser percorrido diretamente, produzindo os campos com que foi construído.
adt iterative Shape = Circle Double | Rectangle Double Double vals = [] for p : Circle(5.0) do vals = vals <> [p] end vals -- [5.0]
Apelidos de importação e acesso qualificado. Uma importação pode receber um apelido curto com as, e qualquer nome importado pode ser alcançado por seu qualificador usando a forma module:function.
import liba as la import libb as lb la:map([1, 2], \z -> z + 10) -- usa o map de liba lb:map([1, 2], \z -> z + 10) -- usa o map de libb
Resolução de conflitos de importação. Quando duas bibliotecas exportam o mesmo nome, esse nome deixa de ser ambíguo. Cada definição continua acessível por seu próprio qualificador, então as bibliotecas conflitantes podem coexistir em um arquivo.
import liba import libb liba:map([1, 2], \z -> z + 10) -- a definição de liba libb:map([1, 2], \z -> z + 10) -- a definição de libb
A biblioteca bernparsec. Uma biblioteca de combinadores de parsing inspirada em Megaparsec entrou nas bibliotecas embarcadas. Ela constrói parsers a partir de peças pequenas e combináveis, como satisfy, pure e fail_parser.
import bernparsec -- construa parsers a partir de peças pequenas e combináveis digit = satisfy(\c -> c >= '0' && c <= '9')
Realce de sintaxe e IntelliSense. A extensão do editor ganhou realce de sintaxe completo, com coloração de palavras-chave e tipos, além de autocompletar com IntelliSense enquanto você digita.
Nix flake. Um Nix flake foi adicionado para builds e ambientes de desenvolvimento reproduzíveis, contribuído por ProggerX.
nix build # compila o Bern a partir do flake nix run . -- app.brn # roda um script com essa build
A série 1.1 trouxe uma interface de função estrangeira (FFI) em C funcional e uma experiência de relato de erros muito melhor.
FFI universal em C. O Bern ganhou uma interface de função estrangeira de verdade para C que funciona da mesma forma no Windows e no Linux, substituindo os antigos stubs improvisados. Uma declaração foreign nomeia o símbolo, a biblioteca de onde carregá-lo, seus tipos de argumento e seu tipo de retorno, e depois é chamada como qualquer função Bern.
foreign rand("msvcrt.dll") -> "int" foreign srand("msvcrt.dll", "int") -> "void" srand(42) rand() -- um inteiro aleatório gerado pelo C
FFI ciente de ADTs e bindings da Raylib. Chamadas estrangeiras agora podem ler e retornar tipos de dados algébricos, não apenas números primitivos. Isso tornou possível entregar um conjunto quase completo de bindings da Raylib para gráficos e entrada.
import raylib -- ADTs atravessam a fronteira da FFI, então cores, vetores -- e retângulos podem ser passados direto para os bindings em C InitWindow(800, 450, "Bern + Raylib")
Atribuição de variável global. O operador := atribui no escopo global a partir de qualquer lugar, inclusive dentro do corpo de uma função, então uma função pode atualizar estado compartilhado em vez de apenas suas vinculações locais.
counter := 0 -- cria ou atualiza uma vinculação global def tick() -> counter := counter + 1 tick() counter -- 1
Tracebacks melhores. Erros de execução agora imprimem um traceback que mostra a cadeia de chamadas que levou à falha, com arquivo e linha de cada quadro, tornando a origem de um problema muito mais fácil de achar.
oops() -- Traceback (chamada mais recente por último): -- em oops (app.brn:3) -- em main (app.brn:7) -- Erro: divisão por zero
NaN em atribuições. Expressões que avaliam para NaN, como dividir zero por zero, agora podem ser atribuídas e levadas adiante em cálculos posteriores, em vez de interromper a avaliação.
x = 0 / 0 -- NaN, atribuído sem interromper a avaliação
Biblioteca de leitura de CSV. Uma biblioteca de CSV entrou nas bibliotecas embarcadas. read_csv carrega um arquivo e retorna suas linhas, com controle opcional sobre o delimitador, se a primeira linha é um cabeçalho e se linhas vazias são ignoradas.
import csv rows = read_csv("data.csv") -- delimitado por vírgula, primeira linha é cabeçalho read_csv("data.tsv", "\t", false, true) -- delimitador personalizado, sem cabeçalho
Biblioteca random. Uma biblioteca random envolve a biblioteca padrão do C pela nova FFI para fornecer números aleatórios com semente, incluindo auxiliares como get_random_int e random_choice.
import random get_random_int() -- um inteiro aleatório random_choice(['apple', 'banana', 'cherry']) -- um elemento ao acaso
get_host_machine(). Uma nova função global informa a plataforma do host, que as bibliotecas usam para escolher a biblioteca nativa certa para carregar em tempo de execução.
get_host_machine() -- ex.: "mingw64" no Windows, usado para escolher uma biblioteca C
O primeiro Bern público: uma linguagem interpretada, de tipagem dinâmica e orientada a expressões, com funções de casamento de padrões.
Listas e conjuntos. O Bern já nasceu com listas e conjuntos de primeira classe, incluindo operadores de teoria dos conjuntos para união, interseção e diferença, e concatenação para listas.
[1, 2, 3] <> [4, 5] -- [1, 2, 3, 4, 5] (concatenação) {1, 2, 3} |> {2, 3, 4} -- {2, 3} (interseção) {1, 2, 3} </> {2, 3, 4} -- {1} (diferença)
O laço for-in. Uma única palavra-chave for cobria a iteração, usando o separador : para percorrer os elementos de uma coleção.
for n : [1, 2, 3] do n * 10 end
Condicionais. A ramificação usava blocos if/then/else fechados com end, com cadeias else-if para mais ramos.
if age >= 18 then "adult" else "minor" end
Notação de objetos. Objetos, que também servem como hashmaps, eram escritos com a notação #{ ... }# e indexados por chave.
obj = #{ name: "Bern", version: 1 }# obj["name"] -- "Bern"
Tipos de dados algébricos. Os ADTs permitem definir um tipo com vários construtores e então casar padrões sobre esses construtores diretamente nas cláusulas de função.
adt Shape = Circle Double | Rectangle Double Double def area(Circle(r)) -> 3.14159 * r * r def area(Rectangle(w, h)) -> w * h area(Circle(5.0)) -- 78.53975
Strings como listas de caracteres. O tipo dedicado Text foi removido em favor de representar strings como listas de caracteres, então toda operação de lista funciona em texto e uma string indexa para um caractere.
"Bern"[0] -- 'B' ['H', 'i'] -- "Hi"
Módulo. O operador de módulo % retorna o resto de uma divisão.
17 % 5 -- 2
A biblioteca core. A biblioteca core fornecia as funções de ordem superior básicas, como map e filter.
import core map([1, 2, 3, 4, 5], \x -> x + 2) -- [3, 4, 5, 6, 7]
A biblioteca strings. Uma biblioteca de strings dedicada reuniu auxiliares de texto, incluindo conversões de caixa de caracteres.
import strings char_to_upper('a') -- 'A'
Leitura e escrita de arquivos. A entrada e saída de arquivos chegou por read_file e write_file.
write_file("hello.txt", "Hi there") read_file("hello.txt") -- "Hi there"
Entrada do usuário. A função input lê uma linha de texto digitada pelo usuário, opcionalmente mostrando um prompt antes.
name = input("Type your name: ")
Conversões. Funções auxiliares transformavam caracteres e strings em inteiros.
to_int("42") -- 42
Importações. Bibliotecas, embutidas ou escritas por você, eram trazidas com a palavra-chave import.
import core -- uma biblioteca embutida import tutorials/adt -- um módulo escrito pelo usuário, por caminho
REPL interativo. Um laço read-eval-print permitia avaliar expressões uma de cada vez.
> 1 + 1 2
Site de documentação. O site de documentação foi ao ar, publicado automaticamente com GitHub Pages, junto com scripts de instalação de uma linha para PowerShell e Bash.
iex (irm https://bern-lang.github.io/Bern/install/install_bern.ps1)