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.