Kivutarのブログ

A blog about minimalism.

Polka Ported to Perl6

Polka is a small web-app I wrote using Dancer as an exercise in minimalism. It is inspired by werc. Polka recursively reads a directory filled with markdown files, convert and serve them using HTTP, with a nice design and a navigation menu.

My friend Marc asked me to port it to Rakudo Perl6.

It took me a while to figure out all the new tricks, as it is my first Perl6 script. The code may not be perfect for this reason (and I am open to code reviews) but it gave me a first feeling of the so long awaited language.

I’m trying to give a feedback by comparing small blocs of code in the 2 idioms.

First, the dependencies.

Polka depends on Dancer (a web framework) and Text::MarkDown (markdown to html converter).

Perl5
1
2
3
4
#!/usr/bin/perl
use Dancer;
use Text::Markdown 'markdown';
use utf8;

Rakudo Star bundles a clone of Dancer called Bailador. It is far from complete, but ok for our case.

No need to specify use utf8, Perl6 allows utf8 source code by default.

Text::Markdown in Perl6 is not complete either: I found it on Karl Masak github, it makes use of Perl6 Grammars to parse Markdown, but no HTML converter was done. Althrough it may be easy to code, the grammar itself is incomplete. Please contribute to Text::Markdown if you have some free time left.

Perl6
1
2
3
#!/usr/bin/perl6
use Bailador;
use Text::Markdown;

My first good surprise

Perl6 has a builting slurp function (slurp means reading a file in a single time), so the following line vanished:

Perl5
1
sub slurp { local $/; open(my $fh, '<:utf8', shift); <$fh> }

Yet another goodie

In Polka, this recursive function walk through subdirectories and build a hash that I use later to generate the menu HTML markup.

Perl5
1
2
3
4
5
6
7
8
9
10
11
12
sub dirtree {
    my $dir = shift;
    my %tree;

    opendir(DIR, $dir);
    my @files = grep {!/^\.\.?$/} readdir(DIR);
    closedir(DIR);

    $tree{"$dir/$_"} = -d "$dir/$_" ? dirtree("$dir/$_") : '' for @files;

    \%tree;
}

Thanks to Perl6, you don’t need to open and close directories no more. There is a dir() function that does this for you. This function also filters the current “.” and parent “..” directories. The resulting code is a lot prettier.

Perl6
1
2
3
4
5
sub dirtree($dir) {
    my %tree;
    %tree{"$dir/$_"} = .IO.d ?? dirtree("$dir/$_") !! '' for dir($dir);
    %tree
}

Comments:

  • New function signatures
  • ?? and !! is the new ternary operator wich replaces ? and :
  • .IO.d means $.IO.d_
  • No need to escape the hash, Perl6 passes references to structures

The menu sub

This recursive sub takes the preceding hash as argument and build a classic know ul > li menu.

Perl5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sub menu {
    my ($tree, $path, $unfolded) = @_;
    my $menu = $unfolded ? "<ul class=\"active\">\n" : "<ul>\n";
    while ( my ($link, $child) = each %$tree ) {
        $link =~ s/^data\///;
        my $label = pop @{[ split '/', $link ]};
        if ( $child ) {
            $menu .= "<li class=\"dir\">$label\n".menu($child, $path, $path =~ /$link/)."</li>\n";
        } else {
            my $class = $link eq $path ? ' class="active"' : '';
            $menu .= "<li$class><a href=\"/$link\">$label</a></li>\n";
        }
    }
    $menu .= "</ul>\n";
}

Now the Perl6 code:

Perl6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sub menu (%tree, $path = '', $unfolded = False) {
    my $menu = $unfolded ?? "<ul class=\"active\">\n" !! "<ul>\n";
    for ( %tree.kv ) -> $link is copy, $child {
        $link ~~ s/^data\///;
        my $label = $link.split('/').pop;
        if $child {
            $menu ~= "<li class=\"dir\">$label\n" ~ menu($child, $path, $path ~~ /$link/) ~ "</li>\n";
        } else {
            my $class = $link eq $path ?? ' class="active"' !! '';
            $menu ~= "<li$class><a href=\"/$link\"> $label </a></li>\n";
        }
    }
    $menu ~= "</ul>\n";
}

Let’s look at it in details.

Perl6
1
sub menu (%tree, $path = '', $unfolded = False) {

The new function signature forced me to specify a default value for my optionnal argument $unfolded.

%tree is not casted to an array here and behave more like an hashref.


Next chunk. One of the ways to loop over keys and values of a hash.

Perl6
1
for ( %tree.kv ) -> $link is copy, $child {

I first tried something like this:

Perl6
1
for ( %tree.pairs ) { say .key; say .value; }

But .key and .value cannot be interpolated in regexes, so I has to rename them. The .kv method does the trick here. It flattens the hash, and give as much elements as we ask for each loop. In this case 2.

The is copy thing took me a while to understand. It looks like Perl6 for creates read only variables. We have to tell the interpreter that we want copies instead. I don’t know the reasons. But for now I dislike this thing…


Another interesing thing is this:

Perl5
1
my $label = pop @{[ split '/', $link ]};

In this code, I embeds the result of split (wish is a list) in an arrayref, to cast it to an array so pop can pop it. I always hated this in Perl5. I was happy to find that Perl6 is smarter than is predecessor in this case, I was able to rewrite it like the following:

Perl6
1
my $label = $pop split '/', $link;

And finally in a more idiomatic way:

Perl6
1
my $label = $link.split('/').pop;

The web framework callback

This callback captures the entire path after / in the url.

Perl5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
get qr{/(?<path>.*)} => sub {
    captures->{path} ||= 'Home';
    "<html>
        <head>
            <link href=\"/style.css\" rel=\"stylesheet\" type=\"text/css\" />
        </head>
        <body>
            <div>
                <h1><a href=\"/\"><span id=\"main-title\">Polka~</a></h1>
                <nav>" . menu( $dirtree, captures->{path} ) . "</nav>
                <div id=\"page\">" . markdown( slurp 'data/'.captures->{path} ) . "</div>
            </div>
            <footer>Powered by <a href=\"#\">Polka</a>.</footer>
        </body>
    </html>";
};

dance;

And here is the new code in Perl6 using Bailador :

Perl6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
get '/(.*)' => sub ($path is copy) {
    $path = "$path" || "Home";
    "<html>
        <head>
        <link href=\"/style.css\" rel=\"stylesheet\" type=\"text/css\" />
        </head>
        <body>
            <div>
                <h1><a href=\"/\"><span id=\"main-title\">Polka~</a></h1>
                <nav>" ~ menu( %dirtree, $path ) ~ "</nav>
                <div id=\"page\">" ~ slurp( "data/$path" ) ~ "</div>
            </div>
            <footer>Powered by <a href=\"#\">Polka</a>.</footer>
        </body>
    </html>";
}

baile;

Most of this code is not interesting, however, translating the following line gave me a headacke:

Perl5
1
captures->{path} ||= 'Home';

First, I had to understand that the $path created by Bailador is read only. The is copy fixed this first issue.

I thought that the following would work,

Perl6
1
$path ||= 'Home';

but it did not, because an empty $path was evaluated to True!

To understand why, I had to debug like this:

Perl6
1
$path.perl.say;

wich is the new way to dump.

This gave me the reply. $path was an instance of the Match() class wich evaluates to True if the regex matches, wich is always the case, either way we would never have entered the callback.

The fix was to cast $path to a string. An empty string evaluates as False and that allowed me to test it and replace it if empty.

Conclusions on the resulting Perl6 app

  • The code is smaller.
  • It is prettier in most cases.
  • Perl6 is fast enought to power webapps.
  • The error messages are more precise with a nice stacktrace, except when the interpretor gets confused.
  • Perl6 still lacks mainstream libs.
  • Too much types and classes makes the language tricky. This is my opinion.