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");
};

1

There are ways to do this without conditional jumps or other literally conditional instructions, so I'm being particular here.