Archive

Author Archive

Piping made easy

July 4, 2020 Leave a comment

Pied Piper of Hamelin is one of the most powerful superheros ever to walk the earth. He could lead large groups of children away by playing a tune on his pipe. Most parents struggle to lead a single child away from the telly. I’m quite sure that’s why |is called a pipe on *nix. This also means that super hero movies are just fairy tales.

It’s a very powerful tool indeed, allowing to compose programs that can handle data in the form of lines of text. If your program lacks the ability to filter its output you can just pipe to grep. You might even reuse a program you have written for a different purpose to do so.

A pipe is actually a very simple construct. We start two programs and connect STDOUT of the first with STDIN of the second. From the stand point of the programs they are writing to filehandles that where opened without a filename. Raku allows us to do so by using Proc::Async.

my $find = Proc::Async.new('/usr/bin/find', '/usr');
my $grep = Proc::Async.new('/bin/grep', 'lib');

$grep.bind-stdin: $find.stdout;

await $find.start, $grep.start;

That’s a lot more wordy than how Bash is doing it.

find /usr | grep lib

We wont get as dense as Bash. Raku is not a shell scripting language. However, in a operator oriented language we should be able to define an operator that does the binding of STDOUT and STDIN. Preferably, with starting the threads and waiting for them to finish. We want to be able to chain that operator too.

role Shell::Pipe {
    method sink {
        say [self[0].command, self[1].command, "sinking"];
        await self[1].start, self[0].start;
    }
}
my multi infix:«|>»(Proc::Async:D $out, Proc::Async:D $in) {
    $in.bind-stdin: $out.stdout;
    [$out, $in] does Shell::Pipe
}

$find |> $grep
# OUTPUT: [(/usr/bin/find /usr) (/bin/grep lib) sinking]
#         <lots of lines found by find containing 'lib'>

The chaining is the tricky part. We want to handle two cases here. Sink context and list context. Raku allows us to handle a list that is not assigned to anything or has a method called. The runtime will call the method .sink on a bare list. We can use that to tell that we have to start processing the pipe. By defining another operator we can capture the output of the whole pipe as lines of text in an Array.

my multi infix:«|>»(@pipe where @pipe ~~ Shell::Pipe, @array) {
    @pipe[1].stdout.lines.tap: -> $line is raw { @array.push: $line };
    @pipe.sink;
}
my @a;
$find |> $grep |> @a;

In this case we call .sink by hand. Chaining pipes needs a little more work. To use sink context a single pipe returns an Array with a role mixed in. We can use that to write a multi candidate that expects just that as its left operand and a Proc::Async on the right. The tricky part is that I want to be able to handle any odd Proc::Async objects. Both for connecting two of them and one of them to an Array to feed data from. I tried a custom IO::Handle but that failed because Proc::Async.bind-stdin wants to call .native-descriptor. If I feed data from an Array I don’t got that. I believe that’s a Rakudobug because if we call Proc::Async.write it just works. So it clearly don’t really need that native descriptor. I got help finding a workaround. As long as Proc::Async.w is changed befor .start-internal is called, the callback is setup properly. Sadly, that attribute does not have a public write accessor. With the help of the MOP we can write a workaround.

my multi infix:«|>»(@array where @array !~~ Shell::Pipe, Proc::Async:D $in) {
    my $h = Shell::ArrayHandle.new(:@array);
    # HERE BE DRAGONS!
    $in.^attributes.grep(*.name eq '$!w')[0].set_value($in, True);
    my $out = class {
        method command { @array.WHAT.gist ~ ' ↦ ' ~ $in.command }
        method start {
            my $p_out = start {
                LEAVE $in.close-stdin;
                $in.write: ($_ ~ "\n").encode for @array;
            }
            slip $p_out
        }
    }
    # $in.bind-stdin: $h;
    [$out, $in] does Shell::Pipe
}

my $sort = Proc::Async.new('/usr/bin/sort');
my @a;
$find |> $grep |> @a;
# fiddle-with(@a);
@a |> $sort;

If you follow my blog you likely spotted already that this is another quest item in my Bag of Holding. Looks like I got quite close to a module that makes Raku a nice replacement for Bash.

When I started to think of how to implement easy Unix pipes in Raku I expected it to be a daunting task. It wasn’t. I have come to the conclusion that “Raku” is actually a verb with the meaning “making things fall into place”.

Categories: Raku

We can do more

June 26, 2020 Leave a comment

DataKinds proceeds with his educational translations of the way of the Python to the way of the butterfly. His quest is to show how to do stuff in Raku that is done in Python. As always TIMTOWTDI applies.

Looking at the examples one can see that Raku lacks nothing Python can provide. That’s nice to know but why would you want to switch if you can’t do more? So lets explore what Raku can do better.

For a Positional container we want to switch each even element with each uneven element. Flip element 1 with 2, then 3 with 4 and so on. We also want to provide a sensible error message for the case where this is not possible. Rakudo is providing and using the X::Parameter namespace. So that’s a natural place for our custom exception.

class X::Parameter::UnevenElementList is Exception {
    method message {
        'Positional with uneven elements in where clause.'
    }
}

Now we need a sub that takes one Positional in a positional parameter and does a type check on it. We shall move that type check into an aptly named sub. We don’t need to reduce readability if we don’t have to. Using a Callable container in a where clause is not in the docs. I will check Roast later and file an issue if needed.

sub swap-each-pair(@a is raw where &check-for-even-elements ) {
    sub check-for-even-elements {
        .elems %% 2
        || X::Parameter::UnevenElementList.new.throw
    }

    for @a <-> $a, $b {
        ($a, $b) = ($b, $a)
    }
}

my @l = 1..6;
@l.&swap-each-pair;
say @l;

I put the where clause sub into the main sub to not pollute the global namespace of my script. In a module we could stick it outside and reuse it. The argument Array is marked as is raw. That’s a bit redundant because @-sigiled arguments are call-by-reference anyway. By doing so I make it clear that this sub is meant to be a mutator. If you are a Haskell lubber, you may want to look away now. The <-> pointer is applying is rw to all arguments. That allows us to modify elements of @a with a destructuring assignment.

Using a good name for the where clause sub and throwing an exception we get a helpful error message with a stacktrace. We must do so or fear the LTA tag.

Positional with uneven elements in where clause.
  in sub check-for-even-elements at /home/dex/tmp/tmp.raku line 28
  in sub swap-each-pair at /home/dex/tmp/tmp.raku line 26
  in block <unit> at /home/dex/tmp/tmp.raku line 38

Both languages are late bound and don’t have strict type checks. In a large code base that can bite you. In that case it can be very helpful to do type and value checks on strategic points and provide error messages that actually help with debugging.

Raku got lots of features that make the life of module authors easier who want to make the life of module users easier. One might think that the language designers have written CPAN modules before.

Categories: Raku

A Raku riddle

June 24, 2020 Leave a comment

While thinking about DataKinds blog post I came up with a most peculiar riddle. It goes as follows:

Name something that when removed is still defined but never existed.

The answer is an Array element whereby the Array got default values.

my @a is default('fill') = 1,2,3;
say @a[5]:exists; # False
@a = @a[5]:delete;
say @a[5].defined; # True
say @a[5]:exists; # False

How many elements got such an Array and when will iteration end?

say @a.elems; # 3
.print for @a; # 123

It has 3 elements and we can iterate over them. What makes perfect sense. However, if we judge by definedness for looping it will be infinite.

while @a[$++] -> $_ { .say } # will not stop

Is this an infinite list then? It can not run out of elements so if we believe the docs about infix:<Z> it should never stop the zipping.

use v6;

my @a is default('‽') = 1,2,3;
my @b = <a b c d e>;

say @a Z @b;
# OUTPUT: ((1 a) (2 b) (3 c))

I tried to convince Z to behave as desired to no avail. In the process I learned how to set the default value of a container long after its declaration.

my @a := Array.new;
{
    use nqp;
    my $descriptor = nqp::getattr(@a.VAR, Array, '$!descriptor');
    my $a = $descriptor.set_default('‽');
}
say @a[1];
# OUTPUT: ‽

Rakudo does a lot with iterators. Since they are an implementation detail, we can’t easily build our own. That’s not really a problem because the zip operator is clever enough to DWIM.

my @a = 1,2,3;
my @b = <a b c d e>;
say ((|@a, |('‽' xx *)) Z (|@b, |('‽' xx *)))[^(@a.elems max @b.elems)];
# OUTPUT: ((1 a) (2 b) (3 c) (‽ d) (‽ e))

I believe that is what DataKinds was looking for.

Categories: Raku

wat-freeness

June 22, 2020 1 comment

Sadly I can’t attend the conference in the cloud because I lend my wings to an angel and the sucker didn’t bring them back. But there is nothing – not even a plague – that can stop me from writing a blog post.

Conferences tend to be a time of reflection and mug throwing. I aim to do so with this post. Most of my work on Raku went into the docs and this blog (please note the recursive reflection). This was a very selfish act. Without a documentation that does not have holes, learning a language can be quite tricky. The blog helped me a great deal to do the thinking that was required for hole plugging. It is a great tool for contemplation and creating awareness of new but not young language. I can strongly recommend to use a blog for both. The hits on this particular blog went up by about 63% relative to last year. If we can keep up that grown it will take only 54 years for the interest in Raku to exceed the population of the earth.

In my last post I struggled to understand the nature of a not-bug. It took a good nights sleep to figure out why. The reason seems to be that I didn’t have a mental category to put that thing it. To my delight wat turns into culture. In the context of programming a wat is not undefined behaviour. It is not a violation of syntax or semantics. It it an emergent interaction between different subsystems of a language implementation that violates the principle of least surprise. An LTA is a subset of wat. Let’s have a bit of code.

class C { has $.a = s; }
sub s {}
# OUTPUT: ===SORRY!===
#         Null regex not allowed
#         ------> class C { has $.a = s; ⏏}; sub s {}
#         Couldn't find terminator ;
#         ------> class C { has $.a = s; ⏏}; sub s {}
#             expecting any of:
#                 ;

wat.

If we rename the sub we can make this work.

class C { has $.a = a; };
sub a { 42 }
say C.new.a;
# OUTPUT: 42

What happens here is that the Raku grammar is looking for s/// instead of making a mental note to check for the existence of a sub called s at the end of the compilation unit. At first I thought it to be an LTA. So wats can be really deceiving. As such any language implementor should avoid them where ever possible. With Raku we aim to do so.

Do we tell newcomers? Or to phrase this question differently: Do we still suck at marketing? Let’s have a look at raku.org.

The Raku Programming Language

Hi, my name is Camelia. I’m the spokesbug for Raku. Raku intends to carry forward the high ideals of the Perl community. Raku has been developed by a team of dedicated and enthusiastic volunteers, and continues to be developed. You can help too. The only requirement is that you know how to be nice to all kinds of people (and butterflies). Go to #raku (irc.freenode.net) and someone will be glad to help you get started.

This is astoundingly bad. The heading doesn’t fit to the text right underneath it. It explains Camelia not what Raku is. Granted Camelia, although quite pretty, desperately needs explaining. The author of that paragraph is clearly a language implementor addressing other language implementors. The high ideals of the Perl community are not named nor are the high ideals of the language implementors. Fishing for grants is quite important, just not at the front page to the programming language. With this front page we manage to miss the audience in the very first lines one who is (rightfully) interested in the language will read. This needs to change.

Good marketing provides a benefit to all parties involved. We need to find out what we want the world to know about Raku and put it on that page. Much work goes into the weekly news. Big thanks to lizmat and her occasional replacements. Yet, why is this very helpful content not on raku.org? We link to irc-logs. We are bloody coders that have a really nice language with a really nice web framework. Do we use it to provide a community we want to grow with a hub to get what they need? I think we should.

Raku was meant as a rewrite of the language by the community and of the community. The language and its compiler are in a good place already. If we want it to become a replacement for all dynamic languages, we need to up our game in the marketing department. Changing the name of the language was the easy part of this endeavour.

I hope you, the kind reader of this blog, can make something good of my ramblings. Maybe even take some of it to a conference. In fact, if you read this blog post while attending the conference of strange times, I will consider myself hugged.

Categories: Raku

It felt like a bug

June 20, 2020 1 comment

IO::Path provides quite a few bits of information about a file. What is missing is it’s true type. File endings can be deceiving. On operation systems that sport file we can get a clear picture. Shelling to get the mimetype with file -b -i is a bit slow so I tried to add a .hyper.map on a list of filenames and mixed in a role to IO::Path. I stepped on something that felt like a bug but wasn’t. Following is the golfed code to illustrade the probem.

my @list = 1..10;
my @results;
@results = @list[^24].hyper(:degree(2), :batch(1)).map({
    my $type = run('/bin/ls', :out).out.slurp.chars;
    $_ but role Mime { has $.mime = $type }
});

Rakudo seems to try to form a closure with the role but frees the data that is stored somewhere. This is only noticible when threading slows things down. It is a rare case where the syntax is valid but the semantics are wrong. So the grammar that is Raku can parse it but the compiler can`t do anything usefull with that match. It somehow speaks highly of the compiler to handle this case without crashing. Yet, a warning would be nice. As jnthn pointed out, this is not the right way to use a role.

Of cause there is a proper way to handle such a case. We need to define a role with a parameter that we can use as a default to our attribute.

sub get-filetype(IO() $_){ run('/usr/bin/file', '-b', '-i', .basename, :out).out.slurp }
role Mime[$type] { has $.mime = $type }
@results = @list[^24].hyper(:degree(2), :batch(1)).map({
    my $type = run('/bin/ls', :out).out.slurp.chars;
    $_ but Mime[$type]
});

I did not manage to use an anonymous parametric role. We can define one, store it in scalar but can’t instantiate it. Using the subscript [] on a scalar (and many other things) will tell the grammar to assume we want to work with a Positional.

my $R = role :: [$t] {};
dd $R;
dd $R[42];
# OUTOUT: <anon|10> $R = <anon|10>
#         Could not instantiate role '<anon|10>':
#         Too few positionals passed; expected 2 arguments but got 1

There is a way to use the MOP to do so. I did find the right method in Metamodel::ParametricRoleHOW but failed to figure out how to use it. (If you do know how to use .^parameterize please provide a code snippet.)

Not really closure forming roles seem to be a case where the grammar can handle it but the compiler doesn’t. Which leaves the question how one is supposed to document a thing that should not exist. It is a trap and we have a section for those. But this is placed pretty far from the description of roles. The first two pull requests are written and more will need to follow.

I’m glad that the docs are written in a language that supports hyperlinks. It would be pretty difficult to describe the network that is Raku in a linear thing like a book.

Categories: Raku

Swarms of Exceptions

June 14, 2020 1 comment

According to Larry lazyness is a virtue. Let’s see if Rakudo agrees.

sub virtue { }; lazy virtue;

# OUTPUT: Nil

A few posts ago I lamented about the lack of Exceptions thrown by run, shell and qx{}. I since changed my mind. Neither of those constructs have any idea what OS they are running on or what a shell command even is. The programmer does (hubris).

This left me impatient. The compiler refuses to create an exception for me when Proc.exitcode is non-zero. In Raku that’s a solvable problem.

my $Exception = Metamodel::ClassHOW.new_type(:name($cmd-name));
$Exception.HOW.add_parent($Exception, Exception);
$Exception.HOW.add_attribute($Exception, Attribute.new(:name<$.exitcode>, :type(Int), :package($Exception), :has_accessor));
$Exception.HOW.add_method($Exception, 'message', my method { 'Shell command ⟨' ~ self.^name ~ '⟩ finished with non-zero exitcode of ' ~ self.exitcode ~ '.' } );

$Exception.HOW.compose($Exception);

We use the MOP to create a type at runtime and fill it with attributes and the message method that all Exceptions need. To be able to use this new type in the rest of the program, we need to have that runtime to happen before the rest of the program is compiled. To do so we can use a BEGIN phaser. We can then stuff the new type into an appropriate package.

BEGIN {
    package X::Shell {}
    X::Shell::<ls> := $Exception;
}

As we have access to OUR at BEGIN-time we can also add subs to the script’s scope that can be used in the rest of the program or add our shell commands to a dedicated package to avoid conflicts.

Shell::«$cmd-name» := sub (|c) {
    my $proc = run $cmd-name, |c, :out, :err;
    my $e = X::Shell::«$cmd-name»;
    $e.new(:exitcode($proc.exitcode), :stderr($proc.err.slurp)).throw if $proc.exitcode != 0;

    $proc.out.slurp;
}

Since we put exceptions into X::Shell we can handle exceptions for all shell commands or just a specific one.

Shell::ls('/home/not there', '-l');

CATCH {
    when X::Shell::ls { warn .stderr, .backtrace }
    when X::Shell { warn .^name, ': ', .message; .resume }
}
# OUTPUT: ls: cannot access '/home/not there': No such file or directory
#
#      in block  at exceptional-run.raku line 48

By capturing STDERR and stuffing it into an exception I can choose to expose the error message issued by the shell command to the user of my program and show a stracktrace if I like. The latter can help greatly with debugging. Since I’m really good at writing bugs that will come in handy.

The whole example script can be found here.

Dynamic languages come with a noticeable speed penalty. When we execute them. When we write them we can progress much faster.

Categories: Raku

The Topic is the topic

June 12, 2020 Leave a comment

I lamented in my last post that we got a lack of beginners material. The reason I gave is that writing such guides is hard. Which leaves the question why it is that hard. While plugging holes in the docs I found that many features of the language pop up in many places. Raku is intertwined with itself. For a beginner you would want to start with something simple and than build on that until you reach maximum complexity. What could be more simple then then humble if statement? A lot, actually. And the reason is the Topic. This shall be the topic of this post.

As one can expect the Topic shows up in Raku in many places. The uninitiated might even think it apears like magic. So lets add a magic variable to a Callable.

my $magic = 42;
sub s( $hmm? is raw = OUTER::<$magic>) { $magic = $magic == 42 ?? 'answer' !! 'nope' }
s;
say $magic;

# OUTPUT: answer

That’s exactly how a Block gets handed over the Topic.

my &block = {;}
say &block.signature;

# OUTPUT: (;; $_? is raw = OUTER::<$_>)

Many language elements in Raku set the topic by default. If does so only when we ask for it.

if 42 { $_.say }
if 42 -> $_ { $_.say }

# OUTPUT: (Any)
#         42

How does the compiler know the difference?

my &pointy = -> $_ {;};
say &pointy.signature;

# OUTPUT: ($_)

The explicit Parameter in a pointy block is not optional. We can use arity and count to tell them apart.

say [&block.signature.arity, &pointy.signature.arity];
say [&block.signature.count, &pointy.signature.count];

# OUTPUT: [0 1]
#         [1 1]

There is another way how to change the arity of a block.

&block = { $^a.say };
say [&block.signature, &block.signature.arity];
if 42 { say $^a }

# OUTPUT: [($a) 1]
#         42

Still not conviced the compiler doesn’t cheat?

if 42 { say $^_ }

# OUTPUT:
# Redeclaration of symbol '$^_' as a placeholder parameter
# at /home/dex/projects/blog/topic-is-topic.raku:69
# ------> if 42 { say $^_⏏ }

Which doesn’t mean the compiler wont cheat. It’s the compiler after all. But this is one of those places in Raku where things fall into place. By having a language called Signature to describe parameters of Callables and access to the scope no magic is required to implement magic variables.

This blog post was sparked by a hole in the docs. By writing this post I spotted another one. We forgot the mention that the literal for an empty block is {;}. All we would need to do to find all holes is going through Roast and see if anything mentioned there is also in the docs. Sadly I got other things to do in the next 900 days or so.

But enough moaning. Let’s end with a positive conclusion. Raku may look like magic. It isn’t. It’s just very clever indeed.

Categories: Raku

Replacement similarities

June 8, 2020 3 comments

In my last post I stated that hits on posts of my blog went up by 20% compared to last year. The reason I checked is clearly vanity. The reason I checked on that day is an article about the increasing popularity of Rust. As a spiritual exercise I turned the different reasons given by programmers who ditch the language they use into a check list.

[X] genuinely new features
[X] rule of least surprice (mentioned often)
[ ] no footguns
[x] language designers listen to community
[x] complete tooling (mentioned often)
[X] central repo for libraries/modules
[ ] ideomatic code is fast
[X] FFI interface
[x] many platforms support
[X] memory sefety/type safety/data race safety
[ ] fast/low memory footprint (mentioned often)
[X] backward compatible to itself
[ ] proper error handling
[ ] does not force OOP

The items are from the article and apply to Rust. The ticks are where I see Raku excels to. A capital X is a strong match for us. The domain of the two languages are quite different. I strongly doubt anybody would like to write code for a microcontroller in Raku. Yet what programmers care about forms a sizeable overlap.

Another aspect of the Rust article is the general tenor of the participants of that questionary. All users come from a different language and where driven by pain to reach safer grounds. That means all of them are replacing $language with Rust and like it that way. The success of Rust lies as much in getting things right as in other languages getting things wrong. That was pretty much the goal for Perl 6 in the year 2000. We can tick that box too.

The core team of Rust sees a very strong open community but little resources for beginners. The same is still true for us. Maybe because writing beginners guides is really really hard. We have the same weakness. There are plenty of books for Raku already. I never really liked that idea. I can understand why a contributor with an academic background would want to write a book. You aren’t really an academic if you don’t. I can also see why academics don’t really have a problem with paywalls. And yes, books are paywalls. I used to climb that wall too. There is Stroustrup’s The C++ Programming Language in a bookshelf right behind me. Yet I doubt that a paywall is going to get us more users. A beginners guide must be online and to be reached by one click from raku.org.

There are also differences. Rust was meant to replace C/C++ as a tool to make security critical software at Mozilla. When Raku was release we didn’t really want it to replace anything. Not because there are plenty of dynamic languages out there that need replacing real quick in a hurry. Anybody here who loves PHP? Fancy another outtake of Node.js because a module gets pulled? No, we didn’t want to replace things because we want to be nice. The whole point of marketing is to be nice to the folk you want to reach and be nasty to your competitors. And it’s not like those Rust folk are actually good at marketing. When I read rust I think of old oil barrels, decaying machines and another unwelcome reason to wash my hands. Names seam not to matter all that much for programmers.

At the beginning of 2018 I gave a very brief motivational speech. I still stand by what I wrote back then. Yet I’m not happy with the success we had. Maybe it’s time to stop learning from the mistakes of others and have a look of what swiftly growing language communities are doing right.

Categories: Raku

Down the error rabbit hole

June 6, 2020 1 comment

Good error messages are useful because they tell us what we don’t have to check when something went wrong. (see: LTA) I came to that conclusion by trying to replace Bash with Raku. Lets have a look at some innocent piece of code.

my $p = Proc.new;
$p.spawn(<does-not-exist>);
$p.sink;

CATCH {
    default { say .^name, ‚: ‘, .message; .resume }
}

# OUTPUT: «X::Proc::Unsuccessful: The spawned command 'does-not-exist' exited unsuccessfully (exit code: 1, signal: 0)␤»

This error message is wrong. A command that is to be run by exec and derivates can only produce a non-zero exit code if the executable or script is executable, for the OS user accessible, a normal file and not a directory. (There are more but those vary from OS to OS.) As the name of the command suggests it does not exist and therefor can’t produce an exitcode of 1. This bit me shelling out to brtfs to create a snapshot. Because brtfs is not a valid command. It’s a typo. When I finally realised that btrfs works much better 15 minutes had passed. Many unix commands produce different error codes depending on the arguments provided. So this kind of typo can be really confusing when heavy shell scripting is required.

The example above is the only way for Proc::* to produce an exception. That’s troublesome because the language user is not meant to create a Proc object by hand. The subs run, shell and qx{} will never use it in sink context. Instead they will evaluate to False when checked for errors. One can mimic shell scripting by chaining commands with && that way as done in many shell languages. The $shell however will check errno and state ‘command not found’ to aid its users. Rakudo does not do so. We can change that with a few lines of code.

class X::Proc::CommandNotFound is X::Proc::Unsuccessful {
    method message {
        my $symlink = $.proc.command[0].IO.l ?? ' (symlink)' !! '';
        "The command '{$.proc.command[0]}'$symlink was not found."
    }
}

sub use-proc-fatal {
    &run.wrap(-> |c {
        my Proc:D $ret := callsame;
        if $ret.exitcode > 0 {
            with $ret.command[0].IO {
                X::Proc::CommandNotFound.new(:proc($ret)).throw unless .e;
           }
            X::Proc::Unsuccessful.new(:proc($ret)).throw if $ret.exitcode > 0 || $ret.signal > 0;
        }
        $ret
    });
}
use-proc-fatal;
say ‚### run does-not-exist‘;

run 'does-not-exist';
CATCH {
    default { say .^name, ‚: ‘, .message; }
}
run does-not-exist
X::Proc::CommandNotFound: The command 'does-not-exist' was not found.

This is not a proper solution. Firstly Proc should never throw an exception. If at all Raku should fail because that composes better with //. In shell scripting it is quite normal that the execution of a command doesn’t work. Error handling is just part of the game. Since it is so common there should be more and better exceptions. I have a script where I tested a few more cases with a few more exceptions. Having those returned as Failure from run and shell would make qx stop failing silently.

In my backup script I have to wrap each command in a sub and throw exceptions to take advantage of CATCH and LEAVE blocks for error handling and cleanup (there are temporary directories/files and btrfs snapshots). I really wished to have a use Proc::Fatal in the core to help with this task. I believe that needs to be in core because errno is hidden inside Proc inside Proc::Async inside nqp::spawnprocasync inside MVM_proc_spawn_async where I stopped and climbed back up the rabbit hole. In fact I never found the bloody exec*. Also Bash stops being fun after about 50 lines of code. Easy things should be easy.

I’m quite sure I missed a lot of error modes. So this requires futher thought. If you have such please share. If you write a blog post about Raku right now you can expect 500 hits a week. That is 20% up from last year. We are better then the stock market! In a world where money can be printed but jobs can not that’s a real feat.

Categories: Raku

Ungolden silence

May 27, 2020 3 comments

On my quest to replace Bash with Raku I tried to use qx and failed. It didn’t work for me because qx fails the wrong way. Under the hood Rakudo implements it by some grammar magic that forwards to QX in core.c/Proc.pm6 as follows.

sub QX($cmd, :$cwd = $*CWD, :$env) is implementation-detail {
    my $proc := Proc.new(:out);
    $proc.shell($cmd, :$cwd, :$env);
    $proc.out.slurp(:close) // Failure.new("Unable to read from '$cmd'")
}

This will return an empty Str, an non-empty Str or a Failure object. So this can be defined but False, defined but True or undefined but False. We get the undefined case when the shell that shell starts unexpectedly closes its STDOUT descriptor. We can get something that is False even if the command succeeds and we can get something that is True if the comand fails. On Unix silence is gold. Unless you provide -v or --verbose any command that does not need to output should not. If there is something wrong a non-zero exit code is returned and if ever possible something is written to STDERR. QX ignores exitcodes and stderr is forwarded to $*ERR. So we have little chance the catch it after the fact.

This behaviour will create hard to catch bugs. Let’s make a typo.

my $s = qx!True!;
say [$s.?defined, $s.?Bool, $s.?exitcode];
dd $not-proc;
/bin/sh: 1: True: not found
[True False (Any)]
Str $s = ""

Using the defined-or operator wont help here. Nor would try. Does anybody use qx and try?

dex@dexhome:~/projects/raku/rakudo/src$ ack 'QX'
core.c/Process.pm6
82: once if !Rakudo::Internals.IS-WIN && try { qx/id/ } -> $id {

On Windows that might actually fail as expected. On any other platform we need something sane.

sub sane-QX($cmd, :$cwd = $*CWD, :$env, :$quiet) {
    my $proc := Proc.new(:out, :err);
    $proc.shell($cmd, :$cwd, :$env);
    my $stdout = $proc.out.slurp(:close) // Failure.new("Unable to read from '$cmd'");
    my $stderr = $proc.err.slurp(:close);
    $*ERR.print: $stderr unless $quiet;
    if $proc.exitcode != 0 {
        return "" but role QXFail {
            method defined { False }
            method exitcode { $proc.exitcode }
            method err { $stderr }
        }
    }
    $stdout
}

my $sane = sane-QX(‚True‘);
say [$sane.?defined, $sane.?Bool, $sane.?exitcode, $sane.?err];
say sane-QX(‚True‘) // ‚I has a booboo!‘;

I’m not sure if just mixing undefinedness in is the right way. Maybe an exception with the exitcode and STDERR content would be better. That way the error handling can be moved into a CATCH block.

I will ponder this a little longer and then file a bug report. If it turns out the current implementation is desired the docs will need some warning stickers.

Categories: Raku