This is my attempt at a very boring functional programming language. It is boring on purpose. I am trying to explore the boundaries of how simple a language can get before it gets unbearable. In order to stay above my personal threshold for bearability, Homer is statically typed, although structural, has sum types and pattern matching, and supports parametric polymorphism and equi-recursive types. These are the fanciest features the language offers. The implementation comes with a compiler to WebAssembly and an interpreter (based on a CEK machine). (Usage instructions for those can be found toward the bottom of the file.)
Although one might be tempted to think Homer is lazy, that would be far too complicated. Instead, it is as eager as if there were a donut at stake. There are no Haskell-like type classes, no OCaml-like first-class modules, and no Scala-like implicits. There are no higher-kinded types, no subtyping, no existential types, and no GADTs. Monads are a no-no as well. Polymorphism and recursion are only allowed at the top level. There is no Hindley-Milner type inference, only a bidirectional type checker (enriched with some lightweight unification-based inference for the type arguments of generic functions). The syntax is not indentation sensitive but rather uses good old curly braces and semicolons. There is no currying; function arguments have to be separated by commas and wrapped in parentheses instead. There really is nothing fancy.
As a side note, there are also no sum-of-product types. There are only sum types and product types, but because the type system is structural, there's no real need for sum-of-product types anyway.
Here's what the definition of a generic list type, the map function over it, and a function doubling all values in a list look like:
type List<A> = [Nil | Cons({head: A, tail: List<A>})];
fn map<A, B>(xs: List<A>, f: (A) -> B) -> List<B> {
match xs {
Nil => Nil,
Cons(xs) => {
let y = f(xs.head);
let ys = map(xs.tail, f);
Cons({head = y, tail = ys})
}
}
}
fn double(xs: List<Int>) -> List<Int> {
map(xs, fn (x) { 2 * x })
}Nothing about this is surprising or exciting but it is hopefully quite easy to understand. That's the beauty of boringness.
There is one thing in the example that sits right around my threshold for being
bearable: I'm not thrilled that I first have to match on Cons(xs) and then
project out xs.head and xs.tail. Instead, I would prefer to match on
Cons({head = x, tail = xs}). Such record patterns would give the compiler an
opportunity to warn me when I add a new field to a record type and don't (yet)
handle the new field in some places; very much like it can warn me that a
pattern match is not exhaustive because I forgot to mention a variant
constructor. Even though I'm not convinced that arbitrarily nested patterns
still qualify as boring, putting at least one infallible record pattern into a
constructor pattern seems very reasonable from a "I want my code to break
during refactoring" perspective. It also makes the deliberate absence of
sum-of-product types easier to deal with when it comes to pattern matching.
There's really nothing challenging behind implementing this, other than that I
might need to generate some fresh names in the compiler, it just hasn't been a
priority yet.
For those who think that the syntax above looks a lot like Rust: You're right, the syntax is heavily inspired by Rust, except for the syntax for closures, which was taken from JavaScript -- and definitely not from Go! Even more so, the whole langauge is implemented in Rust. But nothing from Rust's ownership system has made it over. That would be way too fancy for a boring language like Homer.
To play with Homer, you need to install a Rust toolchain. Afterward, you can run a simple example adding up the first 1000 numbers with the following command:
cargo run -- interpet examples/bench.dohIf you're adventurous and want to use the compiler to WebAssembly, which is
still in a very early stage, you first need to
install wasmtime.
Then, you can compile and run the same example via:
cargo run -- compile examples/bench.doh
wasmtime -W gc,function-references --invoke main examples/bench.wasmIf you want to sum to, say, 5000, replace the wasmtime command with
wasmtime -W gc,function-references --invoke sum_to examples/bench.wasm 5000(I start getting stack overflows starting from around 5400 on my setup. This is due to a lack of tail call optimization, which is on my TODO list.)
There is also a simple language server that can be started via
cargo run -- serverThe repo also contains a configuration file which adds support for Homer to the helix editor. This can be used by running
cargo build
PATH="$PWD/target/debug:$PATH" hx examples/bench.doh(If you have direnv installed, you don't need to change
the path but can just run direnv allow followed by hx.)

