bea

An ergonomic language that transpiles to Gleam.

Multi-clause functions with guards, early return, ? and else for errors, records and @derive. Bea compiles to plain, readable Gleam, so you keep Gleam's type checker, both compile targets and every package on Hex.

Bea 0.3.0 is self-hosted. Its compiler is written in Bea, and so is this site.

Sparky beta. The compiler works today; the edges are still being sanded.

shapes.bea
pub type Shape {
  Circle(r: Float)
  Rect(w: Float, h: Float)
}

pub def area(Circle(r)) { 3.14 *. r *. r }
pub def area(Rect(w, h)) { w *. h }

pub def describe(shapes: List(Shape)) -> Result(String, String) {
  if shapes == [] {
    return Error("no shapes")
  }
  total = shapes |> list.map(area) |> float.sum
  Ok("total area: #{float.to_string(total)}")
}

pub def main() {
  describe([Circle(1.0), Rect(2.0, 3.0)]) |> echo
}
shapes.gleam, generated
import gleam/bool
import gleam/float
import gleam/list

pub type Shape {
  Circle(r: Float)
  Rect(w: Float, h: Float)
}

pub fn area(arg_0) {
  case arg_0 {
    Circle(r) -> 3.14 *. r *. r
    Rect(w, h) -> w *. h
  }
}

pub fn describe(shapes: List(Shape)) -> Result(String, String) {
  use <- bool.guard(when: shapes == [], return: Error("no shapes"))
  let total = shapes |> list.map(area) |> float.sum
  Ok("total area: " <> float.to_string(total))
}

pub fn main() {
  describe([Circle(1.0), Rect(2.0, 3.0)]) |> echo
}

Bea goes in, Gleam comes out. The Gleam is what you would have written by hand: the clauses become a case, the return becomes a bool.guard, and the imports are added for you.

Why Bea exists

Gleam is a lovely language: a sound type system, friendly errors, and code that runs on the Erlang VM or in JavaScript. Writing it day to day, a few things wear on you.

A function that handles three shapes of input becomes one function wrapping one big case. There is no early return, so a guard clause is use <- bool.guard(…) or another level of nesting. Error handling chains through use x <- result.try(…), which quietly turns the rest of your function into a callback. Decoders and encoders repeat every field of a type by hand. All of it works, and none of it is how you think about the problem.

Bea keeps Gleam's meaning and changes how it reads. It borrows Elixir's multi-clause functions and guards, adds return, ? and else guards, writes record boilerplate with @derive, and drops let. There are no macros, no new runtime and no new type system, just a syntax that lowers to idiomatic Gleam.

  • Gleam's type checker

    Every line is checked by the Gleam compiler. Its errors are rewritten to point at your .bea file.

  • Gleam's ecosystem

    Any Gleam package works with nothing to wrap, and bea publish ships ordinary Gleam packages to Hex.

  • A way out, any time

    bea transpile --out writes your project as a plain Gleam project. Trying Bea costs nothing to undo.

What it feels like

Functions with clauses

Write one def per shape of input, Elixir style. Clauses can match patterns and carry if guards, and an optional body-less head declares the types once.

They compile to a single Gleam function with a case, so recursion in tail position stays tail-call optimised.

def fizzbuzz(n: Int) -> String

def fizzbuzz(n) if n % 15 == 0 { "FizzBuzz" }
def fizzbuzz(n) if n % 3 == 0 { "Fizz" }
def fizzbuzz(n) if n % 5 == 0 { "Buzz" }
def fizzbuzz(n) { int.to_string(n) }

pub def main() {
  [9, 10, 14, 15] |> list.map(fizzbuzz) |> echo
}

Return early

Check the edge cases first and get out. return works anywhere a statement goes: at the top of a function, inside if and case branches, and inside anonymous functions.

The returned expression is only evaluated when its branch is taken.

def reply(guess: Int) -> String {
  if guess == 42 {
    return "Correct"
  }
  if guess == 41 or guess == 43 {
    return "So close"
  }
  if guess > 42 {
    return "Too high"
  }
  "Too low"
}

pub def main() {
  echo [reply(42), reply(43), reply(99), reply(1)]
}

Bubble errors up with ?

expr? gives you the value inside an Ok, or returns the Error from the enclosing function. It works inside any expression, and after a pipeline it applies to the whole pipeline.

Evaluation order is kept, and short-circuiting is respected: in flag and parse(s)?, parse only runs when flag is true.

pub type Server {
  Server(host: String, port: Int)
}

def parse_port(text: String) -> Result(Int, String) {
  int.parse(text) |> result.replace_error("not a port: #{text}")
}

def check_host(host: String) -> Result(String, String) {
  case string.trim(host) {
    "" -> Error("missing host")
    h -> Ok(h)
  }
}

pub def server(host: String, port: String) -> Result(Server, String) {
  host = check_host(host)?
  port = parse_port(port)?
  Ok(Server(host:, port:))
}

pub def main() {
  [server("localhost", "8080"), server("localhost", "http")] |> echo
}

Match or leave, with else

A guard is an assignment that has to match, followed by what to return when it doesn't: Ok(item) = … else Error(…). A plain condition works too: quantity > 0 else ….

With ?, the pattern is for the Ok value and else e -> … binds the error, and any guard can bind what didn't match. The checks read top to bottom and become nested cases, without the callbacks of use, which still works for code that wraps the rest of a function.

pub type Order { item: String, quantity: Int }

pub def parse_order(form: Dict(String, String)) -> Result(Order, String) {
  Ok(item) = dict.get(form, "item") else Error("pick something")
  Ok(text) = dict.get(form, "quantity") else Error("how many?")
  quantity = int.parse(text)? else _ -> Error("#{text} is not a number")
  quantity > 0 else Error("at least one, please")
  Ok(Order(item:, quantity:))
}

pub def main() {
  echo [
    parse_order(dict.from_list([("item", "honey"), ("quantity", "2")])),
    parse_order(dict.from_list([("item", "honey"), ("quantity", "lots")])),
    parse_order(dict.from_list([("quantity", "1")])),
  ]
}

Anonymous functions with clauses

fn { … } is an anonymous function made of clauses. Hand it straight to list.map instead of writing fn(x) { case x { … } }.

pub type Event {
  Click(x: Int, y: Int)
  Key(String)
  Tick
}

pub def main() {
  [Click(3, 3), Key("q"), Key("j"), Tick]
  |> list.map(fn {
    Click(x, y) if x == y -> "diagonal click"
    Click(..) -> "click"
    Key("q") -> "quit"
    Key(k) -> "key #{k}"
    Tick -> "tick"
  })
  |> echo
}

Records, in braces

A type with a single constructor named like the type lists its fields in braces: pub type Player { name: String, level: Int }. It is exactly the long form, so patterns, updates and @derive work the same.

Record patterns can name a few fields and skip the rest with | _. as keeps the whole value, and Player(p | health: 100) copies it with one field changed.

pub type Player {
  name: Option(String),
  level: Int,
  health: Int,
  mana: Option(Int),
}

pub def revive(Player(health: 0, level: l | _) as p) if l >= 10 {
  Some(Player(p | health: 100, mana: Some(100)))
}

pub def revive(Player(health: 0 | _) as p) {
  Some(Player(p | health: 100))
}

pub def revive(_) { None }

pub def main() {
  echo revive(Player(name: None, level: 12, health: 0, mana: None))
}

Derive the boilerplate

@derive(decoder, encoder) writes the functions that follow a record's fields one by one, and Bea code calls them through the type: User.decoder().

The compiler knows no format: an ordinary library, the deriver, decides what the functions do. JSON, HTML forms, your own format. The Gleam is what you would write by hand.

import bea_json/decoder
import bea_json/encoder

@derive(decoder, encoder)
pub type Address { street: String, city: String }

@derive(decoder, encoder)
pub type User {
  name: String,
  age: Int,
  tags: List(String),
  address: Option(Address),
}

pub def main() {
  json = `{"name": "Ada", "age": 36, "tags": ["admin"],
    "address": {"street": "1 Hive Lane", "city": "Kanpur"}}`
  Ok(user) = decoder.from_json(json, User.decoder()) else panic
  echo user
  // A field of the wrong type: the error says which one.
  Error(problem) = decoder.from_json(
    `{"name": "Bea", "age": "two"}`,
    User.decoder(),
  ) else panic
  echo problem
  echo encoder.to_string(User(user | address: None), User.encoder())
}

The Gleam you know, spelled shorter

Everything else is Gleam as written: case, use, pipes, captures, labelled arguments, echo, assert, bit arrays and @external.

GleamBeaWhat changed
let x = 1
x = 1
No let. Patterns that can fail get let assert for you.
#(1, "two")
(1, "two")
Tuples use parentheses.
"Hi " <> name
"Hi #{name}"
String interpolation.
dict.from_list([#("bea", 1)])
%{"bea": 1}
Map literals, and %{m | k: v} to update.
pub type User {
  User(name: String, age: Int)
}
pub type User { name: String, age: Int }
Records list their fields in braces.
Point(..p, x: 1)
Point(p | x: 1)
Record update, Elixir style.
[x, ..rest]
[x | rest]
List patterns and prepends.
use n <- result.try(int.parse(raw))
n = int.parse(raw)?
? unwraps Ok or returns the Error.
use <- bool.guard(n < 0, Error(Nil))
if n < 0 {
  return Error(Nil)
}
Early return, anywhere a statement goes.
case dict.get(form, "name") {
  Ok(name) -> …
  _ -> Error(Missing)
}
Ok(name) = dict.get(form, "name") else Error(Missing)
…
else guards: match or leave, and the rest stays flat.
user_decoder()
User.decoder()
A type's functions, reached through the type.

New in 0.3.0

Bea is written in Bea

The compiler, the formatter, the language server and the bea command are Bea code. The repository is an ordinary Bea project: bea build compiles it, bea test runs its tests, and bea pack turns it into the standalone bea on the releases page. Every new compiler builds itself, and the Gleam it generates for its own source stays the same from one build to the next.

On the right is the heart of it, from src/bea/compiler.bea: parse, write the functions @derive asks for, resolve Type.name, lower the Bea-only syntax, print Gleam.

This site, too

This website is a Bea project as well. Every page is a Lustre view written in Bea and rendered to HTML on Node, and the theme switch, the tabs and the playground are Bea compiled to JavaScript. The playground runs the compiler's own Bea source, compiled for your browser by the same build.

Read the compiler Read this site

src/bea/compiler.bea
pub def compile_module(
  module: ast.Module,
  ctx: codegen.Context,
) -> Result(Compiled, List(Diagnostic)) {
  (derived, derive_errors) = deriving.module(module, ctx)
  (resolved, associated_errors) = associated.module(derived, module)
  (lowered, diagnostics) = lower.module(resolved)
  diagnostics = list.flatten([derive_errors, associated_errors, diagnostics])
  (errors, warnings) = list.partition(diagnostics, diagnostic.is_error)
  case errors {
    [_ | _] -> Error(errors)
    [] -> {
      out = codegen.generate(lowered, ctx)
      Ok(Compiled(out.code, out.mappings, warnings, module))
    }
  }
}
Building Bea with Bea
$ bea build            # your bea builds the compiler
$ scripts/bea build    # which rebuilds itself
$ scripts/bea test     # and tests itself
$ bea pack             # build/bea, standalone

Everything Gleam has

A Bea project is a Gleam project with bea.toml in place of gleam.toml and .bea files in src/. The bea command wraps Gleam's tooling: dependencies come from Hex, builds target Erlang or JavaScript, and Gleam modules and FFI files can live right next to your Bea code.

  • Any Gleam package, straight from Hex: bea add lustre gleam_json
  • Erlang and JavaScript targets
  • Tests with bea test, and one standalone executable with bea pack
  • A language server with Gleam's hovers, completion, go to definition and rename, on .bea files, for VS Code, Neovim and Helix
  • A formatter: bea format
  • Compiler errors, echo output and stack traces that point at your Bea source

The whole toolchain, command by command →

Terminal
$ bea new my_app
$ cd my_app
$ bea add gleam_json
$ bea run
$ bea test
$ bea transpile src/my_app.bea   # show the Gleam
$ bea pack                       # one executable
A Gleam type error, pointing at Bea
error: Type mismatch
   ┌─ src/demo.bea:16:22
   │
16 │   label = "n is " <> n
   │                      ^

The <> operator expects arguments of this type:
    String

Try it without installing anything

The playground runs Bea's real compiler in your browser. Type Bea on the left, read the Gleam on the right.

Open the playground
Beatrix

Meet Beatrix

Beatrix is Bea's mascot: a small, cheerful black hole. Gleam's Lucy is a star, which makes the two of them cousins in the same sky. Whatever you write falls into Beatrix, and Gleam comes out the other side.