Hurl, the Exceptional language
Hurl is a language created for one purpose: to explore a language based around exception handling as the only control flow. It was sparked from conversations between Nicole Tietz-Sokolskaya and friends from Recurse Center whose identities will be withheld for their dignity.
This site contains documentation around how to use Hurl. It also provides some examples and guidance for debugging and answers questions.
Praise for Hurl
It comes highly endorsed:
I, uh, have changed my mind about ever implementing a language based on Hurl. This monstrosity is beautiful, and I must never touch it. I don't want my name associated with this in any way1.
Unfortunately, I decided to make this language a reality. I'm sorry.
is "đŸ¤®" an available quote?
Certified unhingedâ„¢!
Exceptions as control flow have been a well-guarded secret within the professional programming community for years. It's about time someone codified these techniques into a coherent whole.
-Jonathan Watmough
To add more praise, email Nicole (please include positive consent to include said quote on this site).
Source code
The source code for Hurl and for this site are both available in Hurl's repo. Emailed patches are welcome if you find a bug or an error, but you'll need to sign over all rights to the patch: I need to preserve the ability to relicense this and license it commercially.
Licenses
For this project, I considered joke licenses and unfortunate licenses. Ultimately, this software is licensed under the following three very serious licenses:
- Under AGPL-3.0
- Under GAL-1.0 (Gay Agenda License)
- Under a commercial license
You may use the software under any one of these licenses, without regard for the others.
Erika did consent to posting this and provided it in its current form. If someone actually does not want their name associated, I would not include it.
Installation
Hurl is not distributed by any package managers as far as the language maintainers1 are aware. Installing it is straightfowrard as long as your platform supports the standard Rust toolchain.
- If you don't have
cargo
installed, install Cargo. You probably want to have at least 1.73.0-nightly, since that's what the 1.0 release of Hurl was tested on. - Clone the repo:
git clone https://git.sr.ht/~ntietz/hurl-lang
- Install Hurl by running these inside the cloned repo:
cargo install --path .
Usage
Hurl ships with one binary, hurl
, which is both the interpreter and the formatter.
To see the usage instructions for hurl
, any of these should do it.
hurl --help
hurl -h
hurl help
To format code, use the fmt
command and pass in the directory containing the files you want to format.
It will recursively format all files in-place in the passed-in directory.
If you just want to check if formatting is correct, use the -c
option.
# in-place format of all files in src/
hurl fmt src/
# checks if the files are correctly formatted, does not update in place
hurl fmt -c src/
To run code, use the run
command and pass in the file you want to run.
hurl run my_program.hurl
Nicole is the only maintainer, but being all formal about it makes this project sound more serious, doesn't it?
Hurl's syntax
Hurl provides a minimal language based around control flow via exception handling. It provides precious few other constructs, just what we need in order to write useful1 programs.
The core language gives you a few primary things:
- binding and assigning to variables
- defining anonymous functions
- exception handling with two types of exceptions
- all the extras like includes and built-in functions
We'll walk through each to get the basics, and you'll see more in the sections on common patterns and examples.
Binding and assigning to variables
Binding a new variable looks like it does in many languages.
let language = "hurl";
let year = 2023;
And then you can also re-assign, using some math operations. (I even snuck a comment in here.)
let year = 2023;
# happy new year!
year = year + 1;
A few things to note about variables:
- A variable must always be initialized
- Every statement ends with a semicolon
- Variables are dynamically typed
- Assigning will find the variable in the closest containing scope and assign to it
- If you try to assign to a variable that has not been bound, then the interpreter will panic
- Lists can be defined with the
[1, 2, "c"]
notation, and are 1-indexed
Anonymous functions
Anonymous functions use the func
keyword to create a function.
These can be used on their own, or they can also be combined with variable binding to reference them again later.
Here's one example that just adds two numbers together and binds it to a variable for use again later.
let add = func(a, b) {
hurl x + y;
};
Note the hurl
keyword.
We'll cover that, but briefly, it means we're throwing an exception from the expression following it.
Functions cannot pass out a value through any means other than throwing an exception to be caught later on.
(return
does exist, but it does not do what you may expect. I'm trying to avoid saying "return" here in reference to functions...)
Since Hurl is dynamically typed, our add
function can also concatenate strings:
# hurls 3
add(1, 2);
# hurls 2023
add("20", "23");
# panics, because + is undefined for bools
add(true, true);
Functions also can be recursive. They have no way to refer to themselves directly (since their name isn't bound until after they are constructed), but you can use the name it gets bound to since it doesn't resolve names until runtime.
Here's an infinite loop that will terminate once the OS stack overflows:
let overflow = func(depth) {
println("depth: ", depth);
overflow(depth + 1);
};
overflow(1);
If you run this, you'll get the following output:
depth: 1
...
depth: 10901
depth: 10902
depth: 10903
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Aborted (core dumped)
Exception handling
Okay, so now we get to the meat of what makes Hurl special: exception handling. This is our primary construct for control flow. Without exception handling, we can do an infinite loop, but we cannot break out of the loop until the interpreter panics, which is not exactly useful. We also can't do conditionals. But we can do all these things via exceptions.
There are two ways to throw exceptions in Hurl:
hurl
an exception and it will unwind the stack until it finds a handler which catches it. Then execution will continue from that handler.toss
an exception and it will walk the stack until it finds a handler to catch it, then it will walk back to where it was tossed from and resume execution from there.
hurl
ing is what we normally see as exceptions in other languages.
toss
ing is kind of a continuation which suspends execution to run a handler, then resumes.
There are also three ways to catch an exception:
catch (<value>)
will match the value proivded as a literalcatch as <ident>
will catch any value and provide it in the catch body as a bound variable (only in scope for the catch body)catch into <ident>
takes no body and catches any value, assigning it into the provided identifier (which must already be defined in some containing scope)
Here's an example of computing a factorial.
let factorial = func(n) {
try {
hurl n == 0;
} catch (true) {
hurl 1;
} catch (false) {
let next = 1;
try {
factorial(n - 1);
} catch into next;
hurl n * next;
};
};
try {
factorial(10);
} catch as x {
println("factorial(10) = ", x);
};
And the result:
> hurl run examples/factorial.hurl
factorial(10) = 3628800
You can also make closures:
let make_counter = func() {
let x = 1;
let counter = func() {
x = x + 1;
hurl x;
};
hurl counter;
};
let c = func () {
};
try {
make_counter();
} catch into c;
try {
c();
} catch as x {
println(x);
};
try {
c();
} catch as x {
println(x);
};
Now let's see an example with toss
.
This is used mostly for passing multiple values out of the function.
You don't really need it, but it's cute.
let f = func() {
toss "hello";
toss 2023;
hurl "bye-bye";
};
try {
f();
println("unreachable");
} catch as x {
println("caught: ", x);
return;
};
println("done");
When we run it:
> hurl run foo.hurl
caught: hello
caught: 2023
caught: bye-bye
done
toss
isn't particularly useful, and it's probably buggier than hurl
, but again: it's cute, and we stan cuteness.
Extras!
There are a few other things you can do.
- Call built-in functions; refer to the standard library reference for what they are and their parameters.
- Include other files with
include <filename-expr>;
. Note that you can provide any expression that evaluates to a filename, so this can even use more complex expressions, fun! The filename will be resolved relative to the first interpreted file. - Standard arithemtic is defined and works how you'd expect.
Now you have the core syntax, so you can move on to some common patterns and some examples.
For certain values of "useful"...
Common patterns
While writing Hurl code, there are a few common patterns to do control flow that you may find useful.
These are the things you'd get out of the box with other languages, but with Hurl, you're stuck doing them the fun Hurl way.
If-else
Every useful program at some point takes a branch1.
To do that in Hurl, you hurl
the value you want to branch on, then catch
the different values.
Here is how to do an if-else:
try {
hurl 1 < 2;
} catch (true) {
println("wow! you learn something every day!");
} catch (false) {
println("we got a problem");
};
Sometimes you just want to match a particular value, and ignore the rest. To do that, you use a catch-all with an empty block; if you don't catch the exception then it will bubble up and that's a bad time.
let year = 2023;
try {
hurl year;
} catch (2023) {
println("wow, this example will be outdated so soon after writing it!");
} catch as x {
};
Note that you cannot use toss
for a conditional with resumptions.
This probably is supposed to work, semantically, but it doesn't, and I'm not interested in fixing it.
Not really sorry about that, just don't do it.
(But if you do debug that and submit a patch, I'll be grateful!)
Recursion
Recursion is ever present in Hurl. You do it kind of like you expect. I honestly tried to make this harder than it is, but it wound up pretty easy, so I guess yay.
I wanted this to be how you have to do it, passing the function into itself:
let f = func(f_, args) {
# do stuff with the args
f_(modified_args);
};
But because of how Hurl resolves variables, you can just call the function recursively like normal.
let f = func(args) {
# do stuff with the args
f(modified_args);
};
Looping
With recursion and if-else, we can put together loops! There are a few loops that are common. I'll show you one here, and the rest are in the standard library.
This is the most basic kind of loop.
It expects two arguments, body
and locals
.
body
is a function and locals
are the local variables to bind in the body by passing them in as the argument.
body
must, at the end of each call, first toss
the local variables to update for the next iteration, and then hurl
a boolean to indicate continuing or halting iteration.
let loop = func(body, locals) {
let new_locals = locals;
try {
body(locals);
} catch (true) {
loop(body, new_locals);
} catch (false) {
hurl new_locals;
} catch as update {
new_locals = update;
return;
};
};
You can use it like this, to do Fibonacci:
let fib = func(locals) {
let a = locals.1;
let b = locals.2;
let iters = locals.3;
let max_iter = locals.4;
toss [b, a + b, iters + 1, max_iter];
hurl iters < max_iter;
};
try {
loop(test, [0, 10]);
} catch as retval {
println("done");
};
There are ways to do this without conditional jumps or other literally conditional instructions, so I'm being particular here.
Standard library
There are two components to the standard library: built-in functions and a distribution of functionality defined in Hurl.
Built-ins
Built-ins are functions which evaluate as an expression to a value; when you use them you do not have to catch the result but may assign it directly.
These are the built-ins that are defined, with *args
representing any number of arguments:
print(*args)
: prints all arguments with no separators; evaluates tonil
println(*args)
:print
, but with a newline; evaluates tonil
is_digit(x)
: evaluates totrue
ifx
is an ASCII digit, andfalse
otherwiseas_num(x)
: evaluates tox
converted to a number, or panics if that failsas_str(x)
: evaluates tox
as a stringstr_chars(s)
: evaluates to a list containing all the chars that are in the strings
, and panics if called on a non-string argumentstr_lines(s)
: evaluates to a list containing all the lines that are in the strings
, and panics if called on a non-string argumentlen(xs)
: evaluates to the length of the list or string represented byxs
, and panics if provided any other typeslice(xs, start, end)
: evaluates to the substring or sublist ofxs
starting fromstart
and going toend
or the end of the list, whichever is sooner; panics if given a non-list and non-string argumentat(xs, index)
: evaluates to the element ofxs
at theindex
location (remember, it's 1-indexed)set(xs, index, val)
: evaluates to a copy of the listxs
with theindex
th element replaced withval
floor(x)
: evaluates to the value passed in rounded down to the nearest integerceil(x)
: evaluates to the value passed in rounded up to the nearest integerread_file(x)
: evaluates to the content of the file at pathx
as a string, or panics if the file does not exist or cannot be readtrim(s)
: evaluates to the strings
with all leading and trailing whitespace removed; this can be implemented in pure Hurl, but I'm sorry, I was tired while doing the Advent of Code problems okay?
Hurl stdlib
If you check out the main Hurl repo, there are some included files in the lib/
directory which are the standard library.
You can browse them in the repo and new standard library functions are welcome.
Examples
Here are a few small Hurl programs. More examples are in the repo.
Fizzbuzz
include "../lib/loop.hurl";
include "../lib/if.hurl";
let fizzbuzz = func(locals) {
let x = locals.1;
let max = locals.2;
let printed = false;
if(func() {
hurl (x % 3 == 0);
}, func() {
print("fizz");
printed = true;
});
if(func() {
hurl (x % 5 == 0);
}, func() {
print("buzz");
printed = true;
});
if(func() {
hurl ~printed;
}, func() {
print(x);
});
println();
toss [x + 1, max];
hurl ~((x + 1) == max);
};
try {
loop(fizzbuzz, [1, 101]);
} catch as retval {
println("Final: " + retval);
};
Fibonacci
include "../lib/loop.hurl";
# fibonacci in hurl
let fib = func(locals) {
let a = locals.1;
let b = locals.2;
let iters = locals.3;
let max_iter = locals.4;
toss [b, a + b, iters + 1, max_iter];
hurl iters < max_iter;
};
try {
loop(test, [0, 10]);
} catch as retval {
println("done");
};
Factorial
let factorial = func(n) {
try {
hurl n == 0;
} catch (true) {
hurl 1;
} catch (false) {
let next = 1;
try {
factorial(n - 1);
} catch into next;
hurl n * next;
};
};
try {
factorial(10);
} catch as x {
println("factorial(10) = ", x);
};
Huh, all the examples start with "F". Weird.
Debugging and errors
Writing Hurl code is not particularly easy since it's so different from what we usually are writing. Unfortunately, the interpreter is not very helpful when we make mistakes: it simply panics at whatever line in the interpreter, which gives you essentially no useful information about what went wrong.
Here are some of the techniques I found helpful while writing and debugging a few programs in Hurl:
- Start small. If you are doing something with a large input, try a small one first so you can more easily trace the execution.
- Add print statements. You can put print statements everywhere to figure out what values are where and where things are going wrong.
- Limit recursion depth. If you run into stack overflows, this is probably because you've looped too far and blown the stack! The stack size is limited by the operating system so if you run into this, you're going to need to work around it by limiting your recursion depth. Nested loops are a nice way to chunk up iteration and limit the max depth.
And then just read your program again and again and you'll find the error eventually! Or you won't, and that's okay. It might also plausibly be an interpreter bug, so feel free to email me for help debugging.
FAQ
Isn't this just <other-language>?
Hurl makes no claims at being unique or novel. The concepts have been explored elsewhere as well. However, to the best of the author's knowledge, it is the first language to provide just exception handling for control flow and make that the primary construct.
Can I call it Hurllang?
No, the name of the language is Hurl. It is not Hurllang, Hurlling, or any other variation.
Did you make it bad on purpose?
No! I assure you, if this were meant to be a bad language it would be far worse. It is testing out how far we can take one thing ("only exceptions!") and the unpleasant nature of the language is a side effect, not the intention.
Is it production ready?
Please don't. If you must, commercial licenses are available for what I assure you is a totally reasonable price. Reach out.
Why did you do this?
To see if we could! We never stopped to ask if we should.
This was an educational project, and much was learned.