Ecosystem

BEAM interop

Pooka is a newcomer on an old street. Erlang has lived here for decades, Elixir moved in with a lot of plants, and Gleam keeps a very tidy garden. The good news is that everyone speaks the same language underneath: BEAM modules, functions and values.

So Pooka code can be called from all of them, and they can be called from Pooka. This page shows how, with small examples.

Calling Erlang, Elixir and Gleam from Pooka

Use external, and write down the type of each function you need:

module main

external Lists from erlang "lists" {
  reverse(items: List(a)) -> List(a)
}

external Clock from erlang "erlang" {
  system_time() -> Int
}

main() {
  echo Lists.reverse([1, 2, 3])
  echo Clock.system_time()
}

The languages are erlang, elixir, gleam and lfe. Your own foreign files go in the project's ffi/ folder, as .erl, .ex, .gleam or .lfe files.

Pooka trusts the types you write here. Keep externals small, and wrap them in ordinary Pooka functions, so the rest of your code never has to wonder.

Calling Pooka from other languages

1. Build it

pooka build --target prod

This compiles your package and its dependencies into build/prod/erlang, one folder per package, without tests. Libraries do not need a main().

Make those folders visible to the BEAM, for example with ERL_LIBS:

export ERL_LIBS="$PWD/build/prod/erlang"

Once your package is published on Hex, this step is not needed: rebar3, Mix and Gleam download and build it like any other package.

2. Find the names

Here is a small Pooka library, in a package called shapes:

module shapes

class Shape {}

class Circle(radius: Float) is Shape

class Rect(width: Float, height: Float) is Shape

class Point(x: Int, y: Int) {
  moved(dx: Int, dy: Int) -> Point {
    Point(x + dx, y + dy)
  }
}

enum Unit {
  Metres,
  Feet
}

actor Counter {
  var _value = 0

  be add(amount: Int) {
    _value += amount
  }

  value() -> Int {
    _value
  }
}

greet(name: String) -> String {
  "Hello, " + name + "!"
}

area(shape: Shape) -> Float {
  match shape {
    Circle(r) -> 3.0 * r * r
    Rect(w, h) -> w * h
  }
}

circle(radius: Float) -> Result(Shape, String) {
  match radius < 0.0 {
    True -> Error("negative radius")
    False -> Ok(Circle(radius))
  }
}

apply_twice(f: fn(Int) -> Int, x: Int) -> Int {
  f(f(x))
}

And this is how it looks from the other side of the fence:

In Pooka On the BEAM
Module shapes in package shapes Module 'pooka@shapes@shapes'
Module library/shelf in package night_library Module 'pooka@night_library@library@shelf'
Function greet(name) greet(Name)
Constructor Point(1, 2) '$class@Point@new'(1, 2)
Method point.moved(1, 1) '$class@Point@moved'(Point, 1, 1): the object comes first
Actor Counter() '$class@Counter@new'()
Actor method counter.add(5) '$class@Counter@add'(Counter, 5)
Enum variant Unit.Feet The atom 'Unit.Feet'

Private declarations, the ones starting with _, are not exported.

3. Pass the values

Pooka BEAM
Int, Float integer, float
String UTF-8 binary
Bool true, false
Nil nil
List(a) list
#(a, b) tuple {A, B}
Ok(v), Error(e) {ok, V}, {error, E}
Some(v), None {some, V}, none
fn(Int) -> Int a fun of one argument
BitArray binary or bitstring
Objects and actors Opaque: make them with constructors, use them with methods

Result and Option look exactly like Gleam's, so they cross between the two without any translation.

Treat objects and actors as sealed envelopes. Make them with their constructors, use them through their methods, and never build or take them apart by hand. What is inside may change between versions; the names above will not.

Examples

Erlang

M = 'pooka@shapes@shapes',
<<"Hello, Erlang!">> = M:greet(<<"Erlang">>),

P = M:'$class@Point@new'(1, 2),
Moved = M:'$class@Point@moved'(P, 1, 1),

6.0 = M:area(M:'$class@Rect@new'(2.0, 3.0)),
{error, <<"negative radius">>} = M:circle(-1.0),
18 = M:apply_twice(fun(X) -> X * 3 end, 2),

C = M:'$class@Counter@new'(),
M:'$class@Counter@add'(C, 5),
5 = M:'$class@Counter@value'(C).

Elixir

shapes = :"pooka@shapes@shapes"

shapes.greet("Elixir")
#=> "Hello, Elixir!"

point = apply(shapes, :"$class@Point@new", [1, 2])
apply(shapes, :"$class@Point@moved", [point, 10, 20])

shapes.circle(-1.0)
#=> {:error, "negative radius"}

shapes.apply_twice(fn x -> x * 3 end, 2)
#=> 18

Enum variants are atoms with a dot in them, so they need quotes: :"Unit.Feet".

If you use Mix with ERL_LIBS, set prune_code_paths: false in project/0. Otherwise Mix removes paths it did not add itself.

Gleam

Declare objects as opaque types, and bind each function with @external:

pub type Shape

pub type Point

@external(erlang, "pooka@shapes@shapes", "greet")
pub fn greet(name: String) -> String

@external(erlang, "pooka@shapes@shapes", "circle")
pub fn circle(radius: Float) -> Result(Shape, String)

@external(erlang, "pooka@shapes@shapes", "area")
pub fn area(shape: Shape) -> Float

@external(erlang, "pooka@shapes@shapes", "$class@Point@new")
pub fn point(x: Int, y: Int) -> Point

@external(erlang, "pooka@shapes@shapes", "$class@Point@moved")
pub fn moved(point: Point, dx: Int, dy: Int) -> Point
let assert Ok(c) = circle(1.0)
area(c)
// 3.0

LFE

(let* ((shapes 'pooka@shapes@shapes)
       (point (call shapes '$class@Point@new 1 2)))
  (call shapes 'greet #"LFE")
  (call shapes '$class@Point@moved point 1 1))

The enum variant Unit.Feet is written '|Unit.Feet| in LFE.

Different languages, one street. Everybody waves.

Panic locations

panic and todo raise an Erlang error with this reason:

{pooka_panic, Message, Path, Line}

Message and Path are UTF-8 binaries; Line is a one-based integer. The location belongs to the expression itself, including calls in tail position. Error-reporting tools should read these fields rather than depend on a surviving caller frame. Uncaught failures and test failures display the location, and user-facing traces omit the package's private runtime frames.

Older compiled code may still raise {pooka_panic, Message} without source metadata. The runtime keeps support for describing that legacy shape.