Vorpal is a minimal and malleable programming language, which I'm building during my 12 weeks at the Recurse Center. Here's what it looks like:
Comments start with // and end at the end of the line.
// This is a comment.
The only basic data type that Vorpal provides are strings. Strings might be composite data types in other languages, but from the perspective of Vorpal each string is an atomic value: You can check whether one string is equal to another, but there are no operations to manipulate strings. (Vorpal strings are interned, just like symbols in Ruby, atoms in Elixir and keywords in Clojure.)
There are two string-like data types:
// Strings are wrapped in double quotes:
"a string"
#"raw strings can contain " characters"#
##"this raw string contains a #" in the string"##
// Capitalized identifiers are string-like atoms:
Foo
BarBaz
While Foo and "Foo" are both represented as strings under the hood, they are different values. Uppercase identifiers are called atoms, double-quoted strings are just called strings.
Strings and atoms can be applied to each other to form ad-hoc compound data structures without the need to define any types beforehand. An atom Foo applied to the two atoms X and Y can be thought of as a record Foo with two positional fields holding the atoms X and Y.
// A record Foo containing X and Y:
Foo(X, Y)
// A Pair of two other records:
Pair(Section(X, Y), Paragraph(Z))
The empty atom is written [] and represents an empty list. Lists of values can be built by successively applying [] to values, with the first value of the list as the last applied value:
[Foo, Bar, Baz] // ...is the same as:
[](Baz, Bar, Foo) // ...which is the same as:
(([](Baz))(Bar))(Foo)
Function names start with non-uppercase ASCII characters (otherwise they would be atoms). Here's an example of a function f being applied to the two atoms Foo and Bar:
f(Foo, Bar)
Functions can also be applied using infix notation:
Foo f Bar // ...is the same as f(Foo, Bar)
Every function can be applied using infix notation. All infix functions have the same precedence and it is invalid to use different infix functions in the same expression without grouping their parts using (...).
(X f Y) g Z // ...is ok, but X f Y g Z would be invalid.
X f Y f Z // ...is ok and parsed as (X f Y) f z.
Vorpal supports a third way of calling functions, keyword calls, which are especially useful for control structures (which are just regular functions in Vorpal). A function named foo-bar-baz can be called as foo: arg1 bar: arg2 baz: arg3, which Vorpal will splice together as foo-bar-baz(arg1, arg2, arg3).
foo: X bar: Y baz: Z // ...is the same as foo-bar-baz(X, Y, Z)
foo: X bar: Y // ...is the same as foo-bar(X, Y)
foo: Bar // ...is the same as foo(Bar)
This means that a single argument function f(x) can always be called as f: X.
Nested keyword calls must be grouped using (...):
foo: (bar: X baz: Y) qux: Z
Lastly, f(x, y, z) is just sugar for ((f(x))(y))(z), in other words f(x, y, z) is sugar for currying, the repeated application of a function to a single argument.
All variables start with non-uppercase ASCII characters (otherwise they would be atoms). A variable can be assigned using the = function, which expects a binding, which is the name of a variable prefixed with “:”. Here's an example:
:x = Foo // Assigns the atom Foo to the variable x.
:y = x // Assigns the value of the variable x to the variable y.
Why does Vorpal distinguish bindings and variables instead of just using y = x like other languages? In Vorpal, the = function is just a regular function and the :x syntax signals that x is not being used, it is being bound. This might look superfluous here, but will come in handy later on.
New function can be defined using the => function, which binds one or more variables on the left-hand side to a function body enclosed in {...} on the right-hand side:
:x => { f(x) } // A single argument function with body f(x).
[:x, :y] => { f(x, y) } // A function of two arguments x and y.
The {...} part is called a block and can contain variable definitions separated by commas or newlines. A block always evaluates to the last expression in the block:
:x => {
:y = f(x)
:z = g(y, y)
Pair(z, z) // This is the return value of the function.
}
A Vorpal program is a collection of expressions that are implicitly wrapped in {...}.
Recursive functions can be built using the ~> function, which binds a variable to itself:
:f ~> {
// Inside of this block, f is bound to itself.
[:x, :y] => {
f(x, y) // This will call f recursively (and never terminate).
}
}
Apart from =, => and ~>, the most useful function provided by Vorpal are the pattern matching functions -> and match-with:
// match: ... with: ... is a keyword call for match-with
match: x with: [
Foo(Bar, Baz) -> { "just Foo(Bar, Baz)" }
Foo(:x, :x) -> { twice(x) }
Foo(:x, y) -> { "the second element is the value of y" }
]
Notice how y is used as a value, not as a binding. The last clause of the pattern match only succeeds if the second element of Foo has the value y, whereas the first element of Foo is bound to the variable x, not matter what the element's value is.
What happens when no pattern matches the value? This is an example of an effect, which is like an exception that can be resumed by its exception handler (if desired). Effects look like function names, but always end with “!” and do not need to defined before being used. What happens when an effect is called depends on which effect handler has been (dynamically) installed. If there is no effect handler, the execution will stop and the effect will bubble up all the way to the (Rust) host, which can then handle the effect and resume the execution.
// This will bubble up to the Rust host:
throw!("throwing something without a handler")
// This will evaluate to Bar(Twice(x, x)):
try: {
match: x with: [
Pair(:x, :x) -> {
Foo(Twice(x, x))
}
:value -> {
// The not-equal! effect will be handled.
Bar(not-equal!(value))
}
]
} catch: not-equal! as: [:resume, :arg] => {
// We handle it by resuming execution:
resume(Twice(arg, arg))
}
One last thing to point out is that commas and newlines are completely interchangeable in Vorpal. Every (...), [...] and {...} can contain one or more commas/newlines before and after each element (including leading or trailing commas/newlines), but elements must be separated by at least one comma or newline.
// [Foo, Bar]
[
Foo
Bar
]
// f(Foo, Bar)
f(
Foo
Bar
)
// { :x = Foo, :y = Bar }
{
:x = Foo
:y = Bar
}
Commas/newlines are not allowed immediately after keywords ending in “:”.
Want to become a better programmer? Join the Recurse Center!