Skip to content

Perl, data, and work

Perl's own library, the framework's, what to do when a call fails, and work that must not freeze the window.

Perl's own standard library

Where the name is Perl's, perl is the specification. length, substr (the four-argument form too), index, rindex, uc, lc, ucfirst, lcfirst, reverse, join, split, sprintf, chomp, chop, ord, chr, hex, oct, trim, tr///, abs, int, sqrt, sin, cos, atan2, exp, log, **, x, time, sort (with a key on each side too: sort { lc($a) cmp lc($b) }), grep, map, splice, shift and pop as values, a slice (@xs[1 .. 3]), scalar, exists, defined, keys, values, rand, srand, List::Util's sum, sum0, max, min, maxstr, minstr, first, any, all, none, reduce, uniq and shuffle, and POSIX's floor, ceil, fmod and strftime are the language's own, not Rakugan's. So are a heredoc (<<~EOT), \U…\E and \u in a string, until, do { … } while, and a list built from lists ((@a, @b), push @xs, @ys).

Random numbers are perl's own too. Since 5.20 perl carries one generator on every platform, so after srand(42) both runs draw the same rand and deal the same shuffle, and the gate compares them like anything else. An app that never seeds is refused: each of its runs would draw a sequence of its own.

use Rakugan;
use List::Util qw(sum max min shuffle);
use POSIX qw(floor);

class Stats {
    use Rakugan;
    use List::Util qw(sum max min shuffle);
    use POSIX qw(floor);
    field @scores = (3, 5, 8, 13, 21);
    field $line   = "-";

    method summarize {
        my @sorted = sort { $a <=> $b } @scores;
        my $mean = sum(@scores) / scalar @scores;
        my @big  = grep { $_ > 5 } @scores;
        my @text = map { "$_" } @big;
        srand(3);
        my @dealt = shuffle(@scores);
        my @hand  = map { "$_" } @dealt;
        $line = sprintf("mean %.1f median %d min %d max %d floor %d big %s dealt %s",
                        $mean, $sorted[int(scalar(@sorted) / 2)],
                        min(@scores), max(@scores), floor(2.7),
                        join(",", @text), join(",", @hand));
    }

    method view {
        return column(
            text($line),
            button("run", on_click => sub { $self->summarize }),
            spacing => 8,
            padding => 12,
        );
    }
}

run(Stats->new, title => "stats");

The compiled run does not carry perl, so each of those is written once in Rust and linked; the interpreted run calls perl's. That the two agree is not a hope: every one of them is held to a table that perl printed (crates/rakugan-stdlib/tests/expected/, a thousand rows), and cargo test -p rakugan-stdlib compares them. A function whose answer depends on the machine's locale or clock reads UTC in both runs.

The framework's standard library

Files, a database, the network and a few other things the operating system owns come from the framework rather than from Perl, and there one implementation answers both runs: the compiled one links it, the interpreted one reaches the same Rust through the engine's C face. Each name says which layer it is from.

    fs_write_text($path, $body);       fs_read_text($path);
    fs_read_text_or($path, "(none)");  fs_exists($path);
    fs_list_dir($dir);                 fs_make_dir($dir);
    fs_append_text($path, $more);      fs_remove($path);
    fs_app_dir("myapp");
    fs_size($path);                    fs_is_dir($dir);
    fs_modified_ms($path);             fs_read_text_from($path, $offset);

    http_get_text($url);               http_get_text_or($url, "");
    http_post_text($url, $body);       http_status($url);

    jsondoc_get_text($doc, "user.name");   jsondoc_get_int($doc, "items.0.qty");
    jsondoc_length($doc, "items");         jsondoc_has($doc, "user.email");
    jsondoc_get_texts($doc, ["user.name", "items.0.qty"], "");

    strings_to_int($s);                strings_to_float($s);
    clock_format_ms($ms, "%Y-%m-%d");  clock_local_offset_minutes();
    notify_send("done", "the file is written");

A call that can fail comes in two forms: the plain one, which stops the handler when it fails, and the _or twin, which takes what to answer instead. Which one to write is the question of whether a missing file is an error or a default.

A database is the same arrangement, because it is no use unless both runs read the one file the same way:

    sqlite_exec($db, "CREATE TABLE IF NOT EXISTS notes(t TEXT)");
    sqlite_exec($db, "INSERT INTO notes VALUES (?)", [$draft]);
    my @rows  = sqlite_query_text($db, "SELECT t FROM notes ORDER BY t");
    my $count = sqlite_query_int_or($db, "SELECT COUNT(*) FROM notes", 0);

Write ? in the statement and put the values beside it: text a person typed can never become part of the statement that way.

When something fails

A call that can fail has two forms, and the _or twin is the one to reach for first: fs_read_text_or($path, "") folds the failure into a default and asks nothing more. When the reason matters, catch it. try / catch is perl's own, written as perl 5.40 writes it, and $e holds what perl would hand it: the message, then the file and the line of the statement that failed.

use Rakugan;

class Notes {
    use Rakugan;
    field $body  = "(none)";
    field $share = 0.0;
    field $count = 0;
    field $note  = "-";

    method read {
        try {
            $body = fs_read_text("demo/.gate/absent.txt");
            $note = "read";
        } catch ($e) {
            $note = "no file: $e";
        }
    }

    method halve {
        try {
            $share = 100 / $count;
        } catch ($e) {
            $note = $e;
        }
    }

    method save {
        try {
            fs_write_text("/nonexistent/dir/notes.txt", $body);
            $note = "saved";
        } catch ($e) {
            $note = "not saved: $e";
        }
    }

    method view {
        return column(
            text("body: $body"),
            text("share: $share"),
            text("note: $note"),
            row(
                button("read",  on_click => sub { $self->read }),
                button("halve", on_click => sub { $self->halve }),
                button("save",  on_click => sub { $self->save }),
                spacing => 6,
            ),
            spacing => 8,
            padding => 12,
        );
    }
}

run(Notes->new, title => "failing");

Uncaught, a failure stops the handler. The lines before it have taken effect, the lines after it do not run, the app stays up, and the message goes to standard error, in both runs. die "…" fails on purpose; warn "…" writes to standard error and carries on. Each adds at FILE line N. unless the text ends its own line, as perl does, and the compiled run names the same file and line.

Perl's own failures are in the same arrangement: a division by zero, a % by zero and the root of a negative number die with perl's words in both runs, and a try around them catches them. So does every call of the framework's that can fail — a write, a query, a read by JSON path, a clock format — because the library has a form of each that a try can take; the _or twins stay the shorter spelling when the reason does not matter. Three shapes a try does not reach yet: a loop (put the try inside the loop, around the line that can fail), a method that can fail (put the try inside the method), and finally, because the compiled run stops the handler at the failure and has no place that runs after it either way.

Timers and work off the window's thread

Work you want repeated goes to every, declared before run:

my $app = Clock->new;
every(1.0, sub { $app->tick });
run($app, title => "clock");

Both runs tick off one clock: a frame in a window, an advance: step in a script.

A handler that blocks freezes the window. Hand the slow work to task, and say what to do with the answer in on_done:

use Rakugan;

class Jobs {
    use Rakugan;
    field $status = "idle";
    field $answer = 0;

    method start {
        $status = "working";
        task(sub {
            my $total = 0;
            my $i = 0;
            while ($i < 300000) {
                $total += $i % 7;
                $i += 1;
            }
            $total;
        }, on_done => sub ($v) {
            $answer = $v;
            $status = "done";
        });
    }

    method view {
        return column(
            text("status: $status  answer: $answer"),
            button("start", on_click => sub { $self->start }),
            spacing => 10,
            padding => 14,
        );
    }
}

run(Jobs->new, title => "tasks");

Neither call waits. task starts the work; on_done runs on the window's thread once the work has answered, and the handler ends and the window carries on. Nothing inside the work may touch the app's state or the screen — the on_done sub is where that belongs, and it is why the answer comes back as a value rather than the work writing it anywhere.

While you are writing it

rakugan run watches the app's file. Save, and the window picks the edit up: the file is read again, the class with it, and the object the window is holding answers with the new view. A file that does not compile leaves the window on what it had and says so in the terminal, and the next save that does compile takes.

Fields keep their values across a save, because the object does. Adding a field, or changing what one starts as, takes effect the next time you start the app: the object in the window was made before the change.