@std/seq to express Sequential Algorithms in Alan

Alan does not allow arbitrary, or classical, loops or recursion and the preferred way to do iteration is with arrays. However, there are algorithms people need to write that are inherently sequential, and often non-deterministic on the number of steps needed to take, such as with numeric approximation algorithms like Newton-Raphson, or anything written as a recursive function call. Most of the problems developers need to solve do not require this linearity, but for when it really is required, we are reintroducing this power in a controlled manner that still allows the AVM to be able to plan around these algorithms and provide the guarantee of termination. The @std/seq syntax was designed to not feel like classical control flow in order to nudge developers to find better, more deterministic and more parallelizable mechanisms, instead.

Here are the function signatures of the sequential functions that @std/seq provides:

fn next(seq: Seq): Result<int64>
fn each(seq: Seq, func: function): void
fn while(seq: Seq, condFn: function, bodyFn: function): void
fn doWhile(seq: Seq, bodyFn: function): void
fn recurse(seq: Seq, recursiveFn: function, arg: any): Result<anythingElse>

Seq type

@std/seq provides functions for accomplishing many different sequential patterns that depend on a special, built-in Seq type with opaque internals to avoid manipulation after construction:

type Seq {
  counter: int64,
  limit: int64,
}

next_loop.ln

from @std/app import start, print, exit
from @std/seq import seq, next
on start {
  let s = seq(2);
  print(s.next());
  print(s.next());
  print(s.next());
  emit exit 0;
}

next is the simplest to explain: each time you call it, it returns the current counter value wrapped in a Result and then increments it. If you call past the limit, it returns an Error Result.

each_loop.ln

from @std/app import start, print, exit
from @std/seq import seq, each
on start {
  let s = seq(3);
  s.each(fn (i: int64) = print(i));
  emit exit 0;
}

each is almost as simple as next: It simply runs the side-effect function however many times is in the limit of the seq instance. The function provided may or may not take the current iteration counter and must be the following signature:

fn func(): void
fn func(i: int64): void

while_loop.ln

from @std/app import start, print, exit
from @std/seq import seq, while
on start {
  let s = seq(100);
  let sum = 0;
  s.while(fn = sum < 10, fn {
    sum = sum + 1 || 0;
  });
  print(sum);
  emit exit 0;
}

while runs the bodyFn up to the limit number of times, but can abort early if condFn returns false. The signatures of these two functions must match:

fn condFn(): bool
fn bodyFn(): void

doWhile_loop.ln

from @std/app import start, print, exit
from @std/seq import seq, doWhile
on start {
  let s = seq(100);
  let sum = 0;
  // TODO: Still need type inference working for multi-line closures
  s.doWhile(fn (): bool {
    sum = sum + 1 || 0;
    return sum < 10;
  });
  print(sum);
  emit exit 0;
}

doWhile always runs at least once (unless the seq has reached its limit or it was constructed with an initial limit of 0) and uses the return value of the function to determine if it should continue or not, so its bodyFn has the following signature:

fn bodyFn(): bool

recurse_fib.ln

from @std/app import start, print, exit
from @std/seq import seq, Self, recurse
on start {
  print(seq(100).recurse(fn fibonacci(self: Self, i: int64): Result<int64> {
    if i < 2 {
      return ok(1);
    } else {
      const prev = self.recurse(i - 1);
      const prevPrev = self.recurse(i - 2);
      if prev.isErr() {
        return prev;
      }
      if prevPrev.isErr() {
        return prevPrev;
      }
      return ok((prev || 1) + (prevPrev || 1));
    }
  }, 8));
  emit exit 0;
}

recurse allows recursive functions to be defined in alan. This is impossible in alan's grammar, so what is done is special trickery to make it possible. The recursiveFn has the following function signature:

fn recursiveFn(self: Self, arg: any): Result<anythingElse>

The Self type is a special type that the recursive function can use to trigger a controlled recursive call, like so:

const recursiveResult = self.recurse(someNewArg);

Self is another opaque type that the AVM can use to keep track of the function to be called recursively and how deep the recursion has gone so far. The recursiveFn must wrap its value in a Result type because alan may interject and bubble up an error if the recursion limit is reached.