A guided, in-depth tour of the language. Every snippet is runnable - paste it into the REPL or save it as a .brn file. Wherever symbols appear, use the Show written form button to flip the same code into its plain-English equivalent.
-- Output: ...). For a terse reference of every feature and the full standard library, see the Documentation. For a quick taste, the homepage examples are a condensed version of this page.
Bern is a small, functionally-inspired language built around a single idea: code should read the way you think about the problem. Three habits will make the rest of this page click:
print ceremony for quick experiments.if, case, and function bodies all produce a value, so they can be assigned, returned, or piped.a + b and a plus b are the same program. Beginners can write words; once the symbols feel natural, switch freely.Write a value on its own line and Bern evaluates and prints it. There is no main function to set up and nothing to import for the basics.
"Hello, World!" 23 3.14 true
Each of these four lines produces output in order. Strings support the usual escape sequences, and you can build new strings with +:
"Bern" + " " + "rocks" -- Output: "Bern rocks" "Line one\nLine two" -- Output: two lines, because \n is a newline
'n') is a character, while double quotes ("n") make a one-character string. Indexing a string returns characters.
Bind a name to a value with name = value. Bern infers the type; you never annotate it. The four core scalar types are integers, doubles, strings, and booleans.
integer = 2 double = 3.14 helloworld = "Hello, World!" isActive = true
Reading a variable is just writing its name. Because evaluation implies output, the value prints:
helloworld -- Output: "Hello, World!" integer + 40 -- Output: 42
Two prefix operators let you inspect values:
:: (also written typeof) returns the type of a value as a string.:> (also written length) returns the length of a list, set, or string.:: 2 -- Output: "Int" :: "hello" -- Output: "String" :> [1, 2, 3, 4] -- Output: 4 :> "hello" -- Output: 5
Arithmetic works as you would expect. Comparisons return booleans, and the logical operators && / || short-circuit - the right-hand side is skipped entirely when the left already decides the result.
17 % 5 -- Output: 2 (modulo / remainder) 2 + 3 * 4 -- Output: 14 (usual precedence) 3 > 2 -- Output: true 5 >= 5 -- Output: true false && (1 / 0) -- Output: false (right side never runs, so no error) true || (1 / 0) -- Output: true
Negative indexing counts from the end of a list or string - -1 is the last element:
[10, 20, 30][-1] -- Output: 30 [10, 20, 30][-2] -- Output: 20 "Bern"[-1] -- Output: 'n'
Every symbol has a plain-English synonym, so a program can read like a sentence. The symbol and the word are identical to the parser - pick whichever is clearer, and mix them freely.
| Symbol | Word | Symbol | Word |
|---|---|---|---|
+ | 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 |
The same example, twice. Press Show written form on either block to confirm they are interchangeable:
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
Loops also accept in as a synonym for :, and function bodies accept returns in place of ->.
The if / then / else / end form chooses between branches. Chain extra cases with 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."
Conditionals nest naturally. A classic FizzBuzz over a range:
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 as an ExpressionBecause if produces a value, you can use it directly inside a function body or assignment - no temporary variable needed.
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"
It even works inline, with no function around it at all:
label = if true then 1 else 2 end -- label is 1
Inspired by Odin, Bern has a single loop keyword, loop. What you put after it changes how it behaves. (for is accepted everywhere as a backwards-compatible alias.)
Give it a number to repeat a body that many times.
loop 3 do
"Hello, Loops!"
end
Give it a boolean condition and it repeats while that condition stays true - this is Bern's while.
counter = 2
loop counter > 0 do
"Hello, Loops!"
counter = counter - 1
end
loop true loops forever - useful for servers and REPL-style programs (stop it with break or by exiting).
loop true do
"Hello!"
end
Iterate the elements of a list, set, or string with : (or the word in).
text = "Bern"
loop char : text do
"Current char is: " + char
end
Ask for a second binding and Bern hands you the position alongside each element.
text = "Bern"
loop char, index : text do
"Char " + char + " is at index " + index
end
The simplest functions are one-liners: def name(params) -> expression. The expression after the arrow is the return value.
def add(x, y) -> x + y add(2, 3) -- Output: 5
For multi-step logic use a do ... end block and an explicit return.
def sumList(xs) do
total = 0
loop n : xs do
total = total + n
end
return total
end
sumList([1, 2, 3, 4])
-- Output: 10
A function can have several clauses. Bern tries them top to bottom and runs the first whose pattern matches the arguments. This replaces most chains of if.
def sign(0) -> "zero" def sign(n) -> "positive" sign(0) -- Output: "zero" sign(42) -- Output: "positive"
Patterns can be literals (matched exactly), variables (bind anything), or _ (the wildcard, which matches and ignores).
def greet("Alice") -> "Hi, Alice!"
def greet(_) -> "Hello, someone else!"
greet("Alice") -- Output: "Hi, Alice!"
greet("Bob") -- Output: "Hello, someone else!"
Negative numbers work as literal patterns, and a variable repeated in one pattern forces the matched values to be equal:
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"
Lists destructure with the [head | tail] pattern - the basis of recursive functions over lists:
def first([]) -> error("first: empty")
def first([h | _]) -> h
first([10, 20]) -- Output: 10
first([]) -- Output: an error value (see "Errors as Values")
Add when <condition> after the parameters to make a clause apply only when the condition is also true. Guards let one set of patterns split on a value's contents.
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"
Guards work with block bodies and with lambdas too:
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"
Functions are ordinary values. Write an anonymous one with \params -> expression, store it in a variable, pass it around, or return it.
inc = \x -> x + 1 pairSwap = \a, b -> [b, a] inc(10) -- Output: 11 pairSwap(1, 2) -- Output: [2, 1]
A higher-order function takes another function as an argument. map and filter from core are the everyday examples:
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
Call a function with fewer arguments than it expects and Bern returns a new function waiting for the rest. This makes it easy to build specialised helpers on the fly.
import core def add(x, y) -> x + y add2 = add(2) -- a new function: "add 2 to its argument" add2(10) -- Output: 12 add(2)(40) -- Output: 42 (chain the calls directly)
Partial application shines when feeding helpers into map and friends:
import core def add(x, y) -> x + y map([10, 20, 30], add(1)) -- Output: [11, 21, 31]
x )| f passes x as the first argument of f - that is, x )| f is exactly f(x). It lets data transformations read left to right instead of inside-out.
import core [3, 1, 2] )| reverse -- Output: [2, 1, 3]
When the function needs more arguments, the piped value fills the first slot and you supply the rest:
import core [1, 2, 3, 4] )| filter(\x -> x > 2) -- same as filter([1, 2, 3, 4], \x -> x > 2) -> [3, 4]
Pipes are left-associative, so chaining builds a readable pipeline:
import core [1, 2, 3, 4] )| filter(\x -> x % 2 == 0) )| map(\x -> x * 10) -- Output: [20, 40]
2 + 3 )| is_five the 2 + 3 is evaluated first, then piped. You can also pipe straight into a lambda: 5 )| (\x -> x + 1).
Lists are ordered collections written with square brackets. The range syntax [a..b] builds a list from a to b inclusive.
numbers = [1, 2, 3, 4, 5] range = [5..10] -- range is [5, 6, 7, 8, 9, 10]
Bern overloads arithmetic and set-theory operators for lists:
[1, 2] + 2 -- Output: [3, 4] (adds to each element) [1, 2] <> [3, 4, 5] -- Output: [1, 2, 3, 4, 5] (concatenation) [1, 2, 3] <| [3, 4, 5] -- Output: [1, 2, 3, 4, 5] (union, no duplicates)
| Operator | Word | Meaning |
|---|---|---|
<> | concat | Join two collections end to end |
<| | union | All elements of both, deduplicated |
|> | intersect | Only elements in both |
</> | difference | Elements in the first but not the second |
Sets use curly braces and automatically keep their elements unique. They support the same set-theory operators as lists.
{1, 2} + 3 -- Output: {1, 2, 3} (add an element)
{1, 2, 3} |> {2, 3, 4} -- Output: {2, 3} (intersection)
{1, 2, 3} > {2, 3, 4} -- Output: {1} (difference)
Objects map keys to values using the #{ ... }# notation.
obj = #{
key: "value",
hello: "world"
}#
obj
Index with brackets to read, reassign, or add new keys:
obj["key"] = "new_value" -- reassign an existing key obj["new_key"] = "added" -- add a brand-new key
Objects nest, and you can chain indices to reach inside:
obj["inner"] = #{ nested_key: "nested_value" }#
obj["inner"]["nested_key"]
-- Output: "nested_value"
An ADT defines a type as a choice between several constructors, each carrying its own fields. Declare one with adt:
adt Shape = Circle Double | Rectangle Double Double
Constructors are called like functions to build values:
c = Circle(5.0) r = Rectangle(3.0, 4.0)
Pattern matching destructures them, pulling the fields out by name. This is where ADTs and multi-clause functions combine beautifully:
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
A common use of ADTs is modelling "a value, or nothing" safely - no null surprises. Here a division returns None instead of crashing on divide-by-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()
case ExpressionsWhen you want pattern matching inside an expression rather than across function clauses, use case value is pattern = result | ... end. Each branch can carry its own guard.
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!"
Branches can bind fields and return them, or match on the result of :: (typeof). Guards use the same when keyword as function clauses:
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"
The json library parses JSON text into Bern values and serializes them back.
import json
person = json_parse("{\"name\": \"Bern\", \"version\": 3}")
person["name"] -- Output: "Bern"
person["version"] -- Output: 3
JSON objects become Bern objects; JSON null becomes Undefined (test it with is_null); and json_stringify turns a value back into a string:
import json
is_null(json_parse("null"))
-- Output: true
json_stringify(json_parse("{\"items\": [1, 2, 3], \"ok\": true}"))
-- Output: a JSON string
[1, "a", 34.2, "a"] parses to {1, ["a", "a"], 34.2}, and round-trips back to the original array.
Instead of crashing, runtime failures in Bern produce an error value you can inspect with is_error. This keeps a program running and lets you decide what to do.
import core is_error(undefined_fn(1)) -- Output: true (no such function) is_error([1, 2, 3][99]) -- Output: true (index out of bounds) is_error(head([])) -- Output: true (no matching clause) is_error(42) -- Output: false (an ordinary value)
Errors are "poison": they propagate through operators, so a single check at the end tells you whether the whole computation succeeded. You can also build your own with error(...).
import core
is_error(undefined_fn(1) + 100) -- Output: true (poison spreads through +)
oops = error("boom")
is_error(oops) -- Output: true
-- and normal work continues unaffected afterwards
7 * 6 -- Output: 42
Ranges are lazy: [1..1000000000] is created instantly and only computes elements as you ask for them. Length, indexing, and take are all O(1) or proportional to what you actually use.
import core big = [1..1000000000] :> big -- Output: 1000000000 (instant, no allocation) big[5] -- Output: 6 take(5, big) -- Output: [1, 2, 3, 4, 5]
Small ranges behave exactly like ordinary lists, so you never have to think about which one you have:
import core [1..5] -- Output: [1, 2, 3, 4, 5] map([1..4], \x -> x * 2) -- Output: [2, 4, 6, 8]
Pull in extra functionality with import. The core library holds the everyday higher-order helpers like map, filter, and 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]
Other libraries include math, strings, random, json, assert, and more - see the standard library reference for the full list.
input() reads a line from the user, much like Python's input. It always returns text; convert it with helpers from core such as to_int.
name = input("Type your name: ")
name
-- Output: whatever you typed, as a string
import core
age_text = input("Type your age: ")
age = to_int(age_text)
"In five years you'll be " + (age + 5)