Powered by AppSignal & Oban Pro

Syntax basics

livebooks/syntax_basics.livemd

Syntax basics

System.put_env("AL_MNESIA_DISTRIBUTED", "false")

System.put_env(
  "AL_MNESIA_DIR",
  Path.join(System.tmp_dir!(), "al_livebook_#{:erlang.unique_integer([:positive])}")
)

Mix.install(
  [{:al, github: "anoma/AL-Ex"}],
  config: [al: [transaction_programs: [AL.TransactionProgram.Bootstrap], natives: []]]
)

Setup

This notebook is self-contained. The cell above installs AL directly, no separate node, no cookie, nothing to attach to. The first run compiles AL and its dependencies from scratch, which takes a minute or two. Reopening this notebook later is fast, since Mix.install caches by the exact dependency and config combination. State here is throwaway. It lives only for this notebook's session and disappears when the session ends.

If you already have a real AL node running and want the persistent, shared store instead (useful for actual development, or for the command log's multi-session material), see the intro for how to attach to it instead.

Mix.install has to finish running before AL's macros (run, defclass, and the rest) can be brought into scope, so that happens in its own cell here.

use AL

AL.Branch.head()

Goals

An AL program is a sequence of goals, run together as one transaction with run do ... end. A goal either succeeds, maybe binding some variables along the way, or it fails.

run do
  x = 1
end

Lowercase names like x are variables. You never declare them, you just use them, and AL fills them in as it runs.

A goal that fails aborts the whole transaction.

run do
  x = 1
  x = 2
end

x can't be both 1 and 2, so this one fails.

WAM Arithmetic

= is value equality. On structures it unifies, binding whatever variables that takes. Wherever an arithmetic expression appears, at any depth, it means numeric equality: with ground operands it evaluates and binds; with open operands it posts a constraint that narrows as they become known.

run do
  x = 2 + 3 * 4
end

Comparisons are written directly.

run do
  x = 10
  x > 5
  x < 20
end

This section does not cover constraint propagation.

Lists

Lists look like Elixir's, including cons.

run do
  [h | t] = [1, 2, 3]
end

The same pattern shows up directly in method and clause heads later on, not just in =.

Printing values

vm_format writes straight to output. ~a prints a value as-is if it's already a string, or its plain representation otherwise. ~d prints an integer. ~% is a newline.

run do
  vm_format("the answer is ~d~%", [42])
end

There's also a ~o directive for printing objects specifically, covered in working with objects.

Collecting multiple solutions

Some goals can succeed more than one way. member checks whether something is in a list, and if the second argument is still open, it enumerates every element instead of just checking one.

run do
  findall(x, results) do
    member([1, 2, 3], x)
  end
end

findall(template, result) do goals end runs goals, collects every way they can succeed, and gathers template's binding from each into a list.

Running a goal once per solution

forall is findall's side-effecting sibling. It doesn't collect anything, it just runs its body once for every way the condition can succeed.

run do
  forall(member([1, 2, 3], x)) do
    vm_format("~d~%", [x])
  end
end

Control flow

implies picks a branch based on which condition succeeds first, top to bottom, with an optional :else.

run do
  x = 5
  implies do
    [x > 3] -> result = :big
    :else -> result = :small
  end
end

alternative is a plain backtracking choice between two goals. Both sides stay live for backtracking.

run do
  findall(x, results) do
    alternative([x = :a], [x = :b])
  end
end

cut commits to whatever's been decided so far in the current call and discards any remaining alternatives.

run do
  findall(x, results) do
    alternative([x = :a, cut], [x = :b])
  end
end

fail always fails. pass always succeeds without doing anything, useful as an empty branch.

run do
  x = 1;
  pass
end

Inspecting terms

var succeeds if its argument is still open. ground succeeds if a term has no open variables left in it anywhere.

run do
  var(x)
end

Anonymous methods

An anonymous_method is an ephemeral value with a head and body. add_arg returns a new value with one argument loaded, and run supplies the remaining arguments.

run do
  new(
    :anonymous_method,
    %{args: [], head: [a, b, result], body: [result = [a, b]]},
    method
  )
  add_arg(method, :first, partial)
  run(partial, [:second, result])
end