31 December 2015

Christmas is here!

This Christmas, the impossible happened. Perl 6 is officially released.

Since its inception, a lot has occurred. The 2001 September 11th attacks on the U.S. and subsequent wars in Afghanistan and Iraq. Apple makes the iPod. There's a new Pope. New Fallout games. We've gone from PHP 4 to PHP 7, skipping 6 because they couldn't get Unicode working right. Apple made a phone, and it turned out to be quite popular. Python 3 was released. Apple made a tablet. The end of the Space Shuttle program. Duke Nukem Forever got released after only 14 years of development. We've landed rovers on Mars, and are making self-driving cars. Apple made a watch. We had two more Star Wars prequels that sucked, and an Episode VII that was actually pretty good.

And now we have Perl 6.

Understand that it is a completely new language rather than an incremental version upgrade. It's still called Perl because it's still in the spirit of doing things the Perl way, and it's got a shiny new version number because it is shiny and new and modern, but it is by no means intended to be backwards compatible with previous versions (except via magic). It is a clean break. And I'm excited to start learning it.

Here's what we're looking at today:-

I ran into a little hard drive trouble recently (yes, again, it seems to be my thing). There's 4 hard drives in the machine that had the trouble, so I wanted to be able to quickly see which hard drives were which. There is an option you can pass to lshw to just look at disks, but I really just needed a quick reminder that /dev/sdc was the older 250G Seagate drive.

I knew I could do it quite simply in the shell: cat /sys/block/sd*/device/model will tell you the model names of all your hard drives, thanks to the /sys filesystem Linux provides. But I'd really like to see the drive name next to each of those. My zeroeth instinct is to write a quick Bash script. My first instinct is that no, I should do all shell scripting in Perl 5 instead. But with Perl 6 and Rakudo Star's release, I have a new instinct: This is a nice simple example script I could port to Perl 6 and learn about it on the way.

Hoverboards and flying cars

Here's my quick little Perl 5 script to do the job:-

#!/usr/bin/perl

use warnings;
use strict;
use 5.018;

use File::Glob qw/bsd_glob/;
use File::Basename;

# Make the assumption that we're only interested in sda, sdb, etc. Other drive controller types get other naming schemes.
my @drive_paths = bsd_glob("/sys/block/sd*");

foreach my $drive_path (@drive_paths) {
   my $drive = basename($drive_path);   # For display.
   unless (-f $drive_path . "/device/model") {
      say "$drive: Unknown model? Check $drive_path/device/";
      next;
   }

   open my $fh, "<", "$drive_path/device/model" or die "Failed to read from $drive_path/device/model : $!";
   my $model = readline $fh;
   chomp $model;
   close $fh;

   say "$drive: $model";
}

and here's the output:-

$ ./lshd.p5 
sda: ST500LT012-9WS14
sdb: ST500LT012-1DG14

That was the script in Perl 5. What about a first stab at things in Perl 6?

Baby steps:

#!/usr/bin/env perl6

use v6;

# Make the assumption that we're only interested in sda, sdb, etc. Other drive controller types get other naming schemes.
my @drive_paths = dir("/sys/block/", test => /^sd/);

say "Hello World6! " ~ @drive_paths;

My goodness, I actually wrote Perl 6 code. Am I living in the future? It even outputs something:-

$ ./lshd.p6 
Hello World6! /sys/block/sda /sys/block/sdb

Let's go through this line by line.

First line: I'm actually using 'env' in the #! line like I should. Because perl6 is not yet anywhere on my system PATH, I've installed it via rakudobrew into my homedir.

Next: use v6;. This is important to specify in case someone somehow runs your code in Perl 5; it needs to fail as gracefully as possible. Observe the lack of use strict; - Perl 6 is strict by default. Observe the lack of various utf-8 enabling things - Perl 6 is even more Unicode-aware than Perl 5 is, and defaults to it.

Finally: How do we glob for files in P6? there's a handy 'dir' builtin that returns a list of directory entries for us, and although it's not a BSD-style glob, passing in a pattern match via the 'test' named parameter works well for our purposes.

Notice that when we say the output, we are using the new string concatenation operator ~. This has changed in Perl 6, and I imagine it's mostly to free up . for use in method calls. This is probably for the best, pretty much every language uses . notation for doing stuff to objects now. But we'll see that Perl 6 is not afraid to go against established conventions when it's the right thing to do. Perl 5 inherited a lot of legacy syntax from things like sed, awk, and even C. Things have been shaken up considerably with 6.

E-I-E-IO

So we have a list of file paths. Is there an equivalent of File::Basename in Perl 6? Why, yes there is, and it's a built in method on the IO::Path role. Did you think dir was returning a list of strings? Think again! By using the string concatenation operator ~, we coerced it to a string representation, just as if we'd written

say "Hello World6! ", @drive_paths.Str;

What happens if we leave off the .Str call, or call .gist on the list instead? Perl tells us we're dealing with objects.

$ ./lshd.p6 
Hello World6! ["/sys/block/sda".IO "/sys/block/sdb".IO]

Let's mimic our P5 code and loop over those, and get the basename of each. Note that there are fancier ways to call .basename on a list of stuff, but I won't dive into that just yet - we're still learning, we don't want to go nuts, loops are familiar and comfortable. And anyway, the syntax for a 'foreach' style loop is quite different from Perl 5. It now looks like this:-

for @drive_paths -> $drive_path {
   my $drive = $drive_path.basename;   # dir returns IO::Path objects, which we can call .basename on!
   say "$drive: ";
}

Incidentally, this new for syntax is pretty cool. It's using syntax for something which works like a Perl 5 anonymous sub, using the arrow -> followed by the $drive_name variable. Other languages have called these 'lambdas', and as an aside, I remember reading about other languages getting lambdas and everyone being so enthused about it and wow, we can lambdas now. I had to wrap my head around it before realising oh, lambdas are just their fancy name for an anonymous subroutine that takes some parameters? I've been doing that in Perl 5 for years without knowing they were somehow special and deserved a different name and weird syntax!

So Perl 6 uses this arrow notation, "pointy blocks", as a shorthand for writing a bit of anonymous code that works like a sub. There is a bit of semantic difference between this and a real sub; notably, calling return will exit from the sub or method that is wrapping the block, much like you'd expect when working with bare blocks rather than a real 'function'. But I digress. New for syntax is cool and lets you iterate over several things really easily. The nice thing is it uses syntax that mirrors other situations where you want to write a block of code that does something to some series of values.

Slurping and Chomping and other eating noises

The next phase of our porting is to construct a path to the device/model file, open it, read its contents, chomp off the trailing newline, and print it to the screen. Seems like it could be a lot of effort. Not so! Perl 6 has a built-in slurp. We can write things pretty simply as:-

for @drive_paths -> $drive_path {
   my $drive = $drive_path.basename;   # dir returns IO::Path objects, which we can call .basename on!
   my $model_path = $drive_path ~ "/device/model";
   my $model = chomp slurp $model_path;
   say "$drive: $model";
}

Which outputs exactly what our Perl 5 version did.

Still... to many programmers, the sentence 'chomp slurp filename' makes perfect sense; given the filename, read the contents then remove a trailing newline. It's several functions operating on each others' return values, so it's written in reverse order. But could it be expressed more naturally? Well, this is Perl 6 we are talking about, and it still embraces the Perl spirit of "There Is More Than One Way To Do It".

As an aside, this is why Perl appeals to me, because it endorses expressiveness. It trusts that the writer may want to reword things so they flow more naturally (and yes, this also means monkeys on crack can also express their insanity). To go off on a bit of a tangent, while other languages will stomp out what they feel are inconsistencies even if they are nice to have, or deprecate entire flow constructs in the name of "There Is Only One Way To Do It And You Will Like It", Perl gives you so much more freedom. People can and will say Python code looks cleaner to them and Perl like line-noise, but in my opinion writing in Python feels like writing in Latin vs writing in English for Perl. Yeah, English is a language which has borrowed all sorts of words from other languages over the years, words which basically mean the same thing, and the grammar is quite permissive. I argue that it is that freedom of expression that makes it so useful.

Anyway, before I digress further, one way Perl 6 supports this kind of expressiveness is method chaining. We can also write our slurping line as a series of calls on the path we built, in a more natural order:-

   my $model = $model_path.IO.slurp.chomp;

Which looks nicer will depend on your point of view. For small things, I'd definitely use the traditional sub calls, if each sub was taking just one argument. But it's clear that for more complex sequences of transformations, you'd want to express things in the expected order, especially if you needed to supply additional parameters to those transformations.

But this is just our little sample script for learning the basics. Let's keep going slowly.

Taking exception

One thing our Perl 5 script does which our Perl 6 version doesn't yet is actually do some sanity checks to see if the device/model file exists and to provide a message if open fails. But with slurp being so useful, we're not even opening the file ourselves anymore. What can we do in this situation?

If we change the script temporarily to look for /sys/block/ram* devices, we can see what happens. Perl 6 dies with a not unreasonable error message:-

$ ./lshd.p6 
Failed to open file /sys/block/ram0/device/model: no such file or directory
  in any  at /home/james/.rakudobrew/moar-nom/install/share/perl6/runtime/CORE.setting.moarvm line 1
  in block <unit> at ./lshd.p6 line 11

Perl 5 always had an exception handling mechanism, but unless you were using a module like Try::Tiny, it involved eval and the special $@ variable and a bunch of unpleasant edge-cases. Perl 6 has built-in try blocks that provide a more formal exception-handling system that will be familiar to users of other languages. Here's how it would look in our case, where we just want to catch anything bad and don't care exactly what went wrong:-

   my $model_path = $drive_path ~ "/device/model";
   try {
      my $model = $model_path.IO.slurp.chomp;
      say "$drive: $model";
      CATCH {
         default { say "$drive: Unknown. Check $drive_path/device/"; }
      }
   }

Now I don't know about you, but to me the first thing that leaps out at me is that CATCH is written in all caps, while try is lowercase. What is this nonsense?! Why would they make it unsymmetrical like that? The language designers have lost it!

... except, after reading up on some of the reasoning behind it, I can understand. CATCH in all caps really did catch my eye, and so it should - this is not ordinary code that will be executed in the same order as everything else. Much like BEGIN and other related blocks, CATCH may be executed out-of-order with the rest of the code. So it should stand out.

While we're assessing code aesthetics though, it occurs to me we could factor out the model-fetching code into its own sub, and then we wouldn't even need an explicit CATCH. We could then write another sub to go poking around in the /sys filesystem to determine the drive capacity, too - which is the first thing I recognise over the model name anyway. Here's my first stab at the refactor:-

for @drive_paths -> $drive_path {
   my $drive = $drive_path.basename;   # dir returns IO::Path objects, which we can call .basename on!
   say "$drive: " ~ model($drive_path) ~ " " ~ capacity($drive_path);
}


sub model($drive_path)
{
   my $model_path = $drive_path ~ "/device/model";
   try {
      return $model_path.IO.slurp.chomp;
   }
   return "Unknown Model";
}


sub capacity($drive_path)
{
   my $size_path = $drive_path ~ "/size";
   my Int $size;
   try {
      $size = $size_path.IO.slurp.chomp.Int;
      CATCH {
         default { return "Unknown Capacity" };
      }
   }
   # The /sys fs returns capacity in terms of 512-byte blocks, for Reasons.
   $size *= SYS_BLOCK_SIZE;
   return $size div TB ~ " TB" if $size div TB > 0;
   return $size div GB ~ " GB" if $size div GB > 0;
   return $size div MB ~ " MB" if $size div MB > 0;
   return $size div KB ~ " KB";   # Really? kilobytes of hard drive space?
}

First thing about Perl 6 that's really nice to have: subs have method signatures. By default. No more my ($self, $thing) = @_; boilerplate. Of course Perl 5 has had a similar method signatures feature since 5.20... but unless you want to install your own perl locally, many distros are still behind the times. With Perl 6 it's there from day one, and has many amazing superpowers to unlock too.

In the capacity() sub, we do much the same thing as we did in model() - read a file and interpret its contents. But wait! Did I really just declare that variable as my Int $size;? Perl 6 supports gradual typing, so if you don't care you can just have scalars that are pretty much anything, but where it matters you can enforce that you know you're expecting an Int and only want to work on Ints, and likewise for any other supported types.

As for the arithmetic involved - div is performing integer division, and KB..TB,SYS_BLOCK_SIZE are constants I defined at the top of the source.

The Spirit of Giving

It's nice to see that the ol' Perlish {statement} {if} {condition} syntax still works, but perhaps we could rewrite that block to make use of the new given/when syntax Perl 6 introduces?

   given $size {
      when * div TB { return round($size / TB, 0.1) ~ " TB" }
      when * div GB { return round($size / GB, 0.1) ~ " GB" }
      when * div MB { return round($size / MB, 0.1) ~ " MB" }
      default { return round($size / KB, 0.1) ~ " KB" }
   }

For those familiar with other languages, given/when is basically Perl 6's switch. But, like anything else Perl 6, more powerful. There's several new things here: given is supplying the variable $size as context to the when clauses. We want to test if dividing that thing by the constant yields a number greater than 0, so we know that's the suffix that's most suitable. If we were just doing a traditional 'switch' looking for various values, we'd put that literal after when and be done with it - Perl 6 would "smartmatch" against those values. But the integer division operator is a bit more tricky, it's an infix operator, so we need a stand-in for $size (in this case. We could be using given on a more complex expression.). That something is apparently termed the "Whatever Star", *, which was a little hard to find out about until I learned its name.

Also featured in this rewrite: No more archaic sprintf() calls to do some decimal place shenanigans! Perl 6 comes with a fully featured set of operations you can do to numbers, including rounding them off to the nearest 0.1. In my case I want one decimal place to show up in case I get a 1.5TB hard drive or something.

Here's the output:-

$ ./lshd.p6 
sda: ST500LT012-9WS14 500.1 GB
sdb: ST500LT012-1DG14 500.1 GB

That's pretty much it. The full script is here. We're just scratching the surface of what we can do in Perl 6, but it's important to take things slow and do the silly little scripts first. Also, it takes me about 10x longer to blog about a script than just write it in the first place ;)

If this post has piqued your interest, check out perl6.org/getting-started.

Updated: Now with more highlighting thanks to Text::VimColor!

6 comments:

  1. Great stuff. As one of the unofficial cheerleaders for getting more stuff in the Perl 6 ecosystem could I interest you in packaging this as a module :)

    ReplyDelete
    Replies
    1. It's just a little script, though. I don't think it really needs to be a module?

      At any rate, I do intend to make a few nice modules for the new ecosystem. Just gotta learn a bit first.

      Delete
  2. Perl 5 has lambdas as well, they are the nameless subs.

    Perl 6 actually has various lambdas, namely pointy blocks, bare block, placeholder block, Whatever code (which you used in the when expression), nameless sub, anonymous sub (they have a name but are not installed in the namespace.), nameless method, and anonymous method.

    ReplyDelete
    Replies
    1. Apologies if I didn't make myself clear - to be fair, that paragraph was just an 'aside' kind of ramble about the topic and I could have worded things better, but I'm well aware that Perl 5's nameless subs work as lambdas do from other languages. That's why I love it, it taught me the concept so naturally without resorting to Greek letters and fancy mathematical terminology.

      I really want to set up something for my blogging where I can mark up a paragraph as being one of these tangential rambles, and have it go in some floating div that's clearly separated from the main text... it's kind of a flaw in my writing style that I just can't help forking off new threads of discussion, which I then have to massage into the main text somehow.

      Delete
  3. This comment has been removed by a blog administrator.

    ReplyDelete