Notes

RC Week 1:
Vorpal—A Minimal and Malleable Language

This week marks my first week at the Recurse Center, where I got to meet a lot of amazing people and had a ton of interesting conversations. I didn't find a lot of time to work on my programming language, Vorpal, but I got the chance to talk about why I'm building a new language and realized that I'm bad at explaining it. So here's why:

I'm building a minimal and malleable language for end-user programming.

In many ways, Vorpal is supposed to be the opposite of Rust: Vorpal is intended for programming in the small, for incremental development with a rapid feedback cycle. It does not compete with Rust, but is meant to extend Rust symbiotically.

(Nick perfectly described it as “end-user programming for Rustaceans”.)

Minimal

Vorpal is minimal, in the sense that the language provides a small core which can then be extended. This applies both to the syntax and the semantics of the language: Vorpal's syntax is extremely small and regular, there are no built-in keywords or control structures with special syntax, even variable assignment is just a normal function. Vorpal is similar to Lisps in this regard, but Vorpal additionally provides some syntactic sugar that avoids Lisp's parentheses-heavy syntax.

// prefix function call
f(x, y)

// infix function call
x f y

// keyword function call
if: x is: y do: {
    ...
} else: {
    ...
}

Malleable

Not having any privileged control structures or special keywords makes Vorpal malleable: It is easy to make the language fit the problem domain. Even something as fundamental as pattern matching does not need to be built into Vorpal, because it can be defined as a function and then used like any other function.

'first('x, 'y) = {
  match: Pair(x, y) with: (
    Pair('x, 'y) -> { x }
    '_           -> { throw!("expected a pair!") }
  )
}

'x = Pair(Foo, Bar)

print!(first(x))

Effectful

Any symbol ending with “!” is an effect, which can be handled using an effect handler. An effect is like an exception that is resumable: Whenever an effect is called, it stops execution and walks up the call stack until it finds an effect handler that knows how to handle the effect. The effect handler can either abort the current execution (like an exception) or resume the execution with the result of the effect.

// foo does not care how the read! effect is handled:
'foo() = {
    'input = read!()
    match: input with: (
        Ok('contents) -> { contents }
        '_            -> { "" }
    )
}

// we can catch the effect and handle it:
try: {
    foo()
} catch: read! as: ('resume, 'arg) => {
    resume("TestInput")
}

Symbiotic

Effects don't have to be handled inside of Vorpal, it is fine to just let an effect bubble up all the way to the Rust host, who can then handle it and resume the execution. This keeps Vorpal small, while also allowing it to easily hook into Rust's ecosystem.

// read! and print! will be handled by the Rust host:
'file = read!("foo.txt")
match: file with: (
  Ok('contents) -> { print!(contents) }
  Err('_)       -> { print!("Could not read file.") }
)

Reasonable

Vorpal is in many ways a very dynamic language and built with a fast iteration speed in mind, but it also aims to be easy to statically reason about, for both humans and compilers. Runtime reflection is limited by design and Vorpal explicitly distinguishes variables that are being used from variables that are being bound (which are prefixed with a single quote), which keeps Vorpal's macros easy to reason about.

// x is being bound, f and y are being used:
'x = f(y)

match: Pair(x, y) with: (
  Pair('_, z)  -> { "pair with z as second element" }
  Pair('_, '_) -> { "any pair" }
  '_           -> { "any value" }
)

Reloadable

This goal is still aspirational (and one of the topics that I want to focus on at Recurse Center). One of the ideas behind Vorpal is to be able to develop and modify a whole application without once stopping a running program, while at the same time ensuring that the source code remains at all times the single source of truth. This last requirement makes it different from REPL-driven development, where it is easy to get into a state where the state of the REPL does not correspond to the definitions in the source file.

Let's see how far I get!

Want to become a better programmer? Join the Recurse Center!