Rewriting Perl code for Raku Part V

Last week we started to talk about the pack() and unpack() builtins for Raku and Perl. These aren’t terribly common built-ins to use, so I thought I’d take some time to go over these in detail and talk about how I use them and debug files that use them.

As a gentle reminder, OLE::Storage_Lite is a Perl module to read and write a subset of the Microsoft OLE storage format. As part of my effort at the start, I’ve got a “translation” of the original Perl code pounded out, without much thought to whether it’ll work, or really even compile. It looks like the Perl version, but with most of the {} changed to <> and -> changed to ..

What to test first… The reading side seems to be the easiest, because I can check object-by-object to see what the data should look like. Replicating that for Raku becomes essentially fixing the bugs I know I’ve introduced on the way.

Testing testing… is this on?

Before we dive into the Raku code, though, let’s just set up a quick test in Perl. There really wasn’t one to begin with, which is a testament to how well-used the module is. I’ve got a ‘test.xls’ file that I’ve already checked in LibreOffice to make sure it works, so I’ll add a test script that reads the file and checks the root object.

use Test::More;
use OLE::Storage_Lite;

my $root = OLE::Storage_Lite->new( 'sample/test.xls' );
use YAML; die Dump($root);
isa_ok $root, 'OLE::Storage_Lite::PPS::Root';
is $root->No, 0;
is $root->PrevPps, 0xfffffffe;

You might be reading the code and wondering what the heck die() is doing in a test suite. It’s not because in my current copy it’s commented out, but it’s a quick and dirty way to get the data for the Raku version of the file, which looks almost the same.

use Test;
use OLE::Storage_Lite;

my $root = OLE::Storage_Lite.new( 'sample/test.xls' );
die $root.perl;
isa_ok $root, 'OLE::Storage_Lite::PPS::Root';
is $root.No, 0;
is $root.PrevPps, 0xfffffffe;

Notice there’s hardly any difference overall, just a few minor syntax tweaks. And I don’t need to use YAML. But I’ve got a Q&D way to run my code, and since my screen looks something like this:

I’ve got most of what I need in my face. This is all a rather plain TMUX setup, running multiple panes so I can see what’s going on. On the left is vim running in split-screen mode with the Perl and Raku test files open. The rest are shells in the Perl and Raku directories, and some commands to get byte dumps of the files.

I’ve also in my shells set up the following aliases:

alias 5 = "perl -Ilib"
alias 5p = "prove -Ilib"
alias 6 = "perl6 -Ilib"
alias 6p = "prove -e'perl6 -Ilib'"

This way I can run both Perl and Raku test suites with just a few keystrokes, and not have to worry about details such as -I paths. You’re of course welcome to do things exactly the same, completely different, or even radically better than I am, in which case please let me know.

You might notice the use of the language’s old name here. I haven’t changed over to the new binaries yet, but the techniques I’ll talk about here won’t change.

Keeping it Clean

We now have two scripts that should produce the same output, but probably won’t, for any number of reasons. I’ve got a whole article’s worth of things that I had to do to make the new module compile, let alone run. But that’s for a later issue.

Let’s start out with this section, which might be familiar to longtime (ha!) readers.

  $rhInfo->{_FILEH_}->seek(0, 0);
  $rhInfo->{_FILEH_}->read($sWk, 8);
  return undef unless($sWk eq "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1");

This is in Perl, of course. In Raku I’ve chosen to write

  $file.seek( 0, SeekFromBeginning );
  my Str $sWk = $file.read( 8 ).unpack('A8');
  die "Header ID incorrect" if $sWk ne HEADER-ID;

It’s a bit ungraceful to die() inside a module, but this guarantees that execution stops way before it can cause a hard-to-debug problem down the road. The first change is that I’ve refactored $rhInfo->{_FILEH_} out into its own $file variable so I don’t have to repeat references to $rhInfo all over the place, like the original.

Next is using the built-in IO::Handle constant ‘SeekFromBeginning’ instead of the rather anodyne 0 as in Perl. Probably the parent OLE::Storage module looked ahead in the file to determine something before reading in earnest. I’m keeping it here for no good reason other than it might be nice to separate ‘read’ functionality into a different method.

Diving in

The next line will cause some consternation, so I’ll unpack it slowly. The original author used Hungarian notation for their variable names, so the ‘s’ of $sWk means that it’s a string type. I’ve adopted this for the Raku code as well, actually enforcing the variable type without additional code.

File handles have both a fancy lines() method that lets you read files line-by-line, and a raw read() method that lets you read raw bytes. If I stopped right here and just looked at the raw bytes, the code would actually fail, and I’ve talked about why in earlier parts. Suffice to say that read() returns a buffer of uninterpreted bytes, not a string that you have to decode later.

Decoding here is the job of the unpack() statement. It acts just like its Perl counterpart, but is experimental. Lucky for me, it implements enough of the Perl builtin that I can use it to read the entire OLE file.

Now, unlike other builtins (again, keeping in mind it’s experimental,) it’s only available as a method call. There is a version of unpack() that works on multiple arguments, but if you try to call it as a builtin, expect:

===SORRY!=== Error while compiling -e
Undeclared routine:
    unpack used at line 1. Did you mean 'pack'?

This may be fixed in your version, feel free to try it and let me know if I should upgrade 🙂 In any case, the last bit you’re wondering about is the ‘A8’ business as its argument. I think this isn’t explained correctly in the documentation, so I’ll explain in my own way.

read() returns a raw string of bytes, without interpretation. If it sees hex 041, it doesn’t “know” if you meant the ASCII character ‘A’ or the number 41, so it doesn’t interpret the data, it just puts the data into the buffer. It relies on the the Buf(fer)’s pack() and unpack() methods to assign types to the data.

So finally, unpack( "A8" ) pulls out 8 “ASCII” characters and puts them into $sWk. Now I used scare-quotes there because ASCII is a 7-bit encoding, not 8 bits as many people seem to think. It only encodes from 0x00-0x7f, so anything over that isn’t legal ASCII.

Which just means that the “A” of “A8” doesn’t truly correspond to ASCII, but it’s close enough. So, we call unpack( “A8” ) on the buffer that $file.read( 8 ) returns, and get back a string that we can finally check against our header.

Debugging

But what if the header isn’t what we expect? Your first instinct might be to say you must’ve screwed up and sent it the wrong file. Luckily it’s pretty easy to check that, just call $file.slurp.print; That’ll tell you the contents quickly. If it’s text you’ve probably got the wrong file – OLE files do contain text but it’s usually zip’ed or in UCS-2.

Let’s assume though that it’s an actual binary file, and a real spreadsheet that Excel (or LibreOffice in my case) can read. Since the headers don’t match, it must be a different version of OLE that our code isn’t ready to handle.

That means we need to know what the first 8 bytes of the file actually are. We’ve got a bunch of tools at our disposal, but what I want to introduce is hexdump(1) (don’t worry about the (1), force of habit.) Run this command on the file:

hexdump -C sample-file.xls | head -1

This should generate something like this:

00000000  d0 c9 11 a0 af b1 13 d1  00 00 00 00 00 00 00 00  |................|

(original bytes changed to protect the innocent file) The numbers on the left (‘00000000’) tell us how far we are into the file (in hex), the next two groups of 8 are the hex values of the individual bytes of the file, and the dots between ‘|..|’ are where any printable characters would appear, if there were any.

So now we know what the first 8 bytes of this file look like, and we can add (without much muss or fuss) some checks to our original file, and come up with this:

$file.seek( 0, SeekFromBeginning );
my Str $sWk = $file.read( 8 ).unpack('A8');
die "Unknown OLE header!" if $sWk eq "\xd0\xc9\x11\xa0\xaf\xb1\x13\xd1";
die "Header ID incorrect" if $sWk ne HEADER-ID;

This check isn’t in my source, so don’t go looking for it. As far as I know there aren’t any other OLE header strings than what I check for, but then I’m trying to get away without reading the spec. My blood pressure doesn’t need that.

Getting at the details

Of course, binary packed formats contain more stuff than just ASCII strings. OLE was originally written in the days of 16-bit CPUs, so it’s got other ways to pack in data. Let’s look at a fragment of the file format: (not from the spec, this is just my interpretation)

0000: 0xD0 0xCF 0x11 0xE0 0xA1 0xB1 0x1A 0xE1 # header
0008: 0x00 0x09           # size of large block of data (in power-of-2)
000a: 0x00 0x06           # size of small block of data
000c: 0x00 0x00 0x00 0x03 # Number of BDB blocks
0010: 0xff 0xff 0xff 0xfe # Starting block

So, this is the first 20 (0x0010+4) bytes of an OLE header block. You may have already caught on to the fact that there are at least 3 sizes of data here. The first 8 bytes on line 0000 is the header data we talked about ad nauseam.

Next, the header says that a “large” block of data is 2**9 bytes long, and a “small” block of data is 2**6 bytes long, this time in pairs of bytes. Finally we’ve got the number of BDB blocks (whatever those are, probably Berkeley DB) and the starting block’s index number, all in 4-byte chunks.

This means we need to read 2 2-byte chunks and 2 4-byte chunks into memory. This time though, we have to read them as numbers. Once again, unpack() comes to the rescue. Last time we used the ‘A’ character, this time we’ll do something just a little bit different.

Let’s read the documentation for unpack() to see what we can use. Halfway down the page we come to a table which gives us the letter abbreviations for each type of data we can read, and what it is in terms of where it is in memory.

For now, replace the term ‘element’ with ‘byte’ while you’re reading the documentation. We need to read (0x00, 0x09) as a 2-byte integer, so let’s look for “two elements” on the right-hand side. “Extracts two elements and returns them as a single unsigned integer” seems to be what we need.

So it looks like the letter we need to use is “S”, and since we only want to read one at a time, that’s all we need. But the original Perl source uses “v”, so that’s what I’ll use as well.

  $iWk = _getInfoFromFile($rhInfo->{_FILEH_}, 0x1E, 2, "v");
  return undef unless(defined($iWk));
  $rhInfo->{_BIG_BLOCK_SIZE} = 2 ** $iWk;

But as you can see, the Perl source creates a wrapper around the pack() method, much to my annoyance. I’d prefer to simply write this:

$iWk = $file.read( 2 ).unpack( "v" );
%hInfo<_BIG_BLOCK_SIZE> = 2**$iWk;

but to keep things looking as similar to the original Perl code as I can, my code looks like

  my Int $iWk = self._getInfoFromFile( $file, 0x1E, 2, "v" );
  die "Big block size missing" unless defined( $iWk );
  %hInfo<_BIG_BLOCK_SIZE> = 2 ** $iWk;

which is just one line longer, and that’s because of the safety check. Of course, pack() and unpack() can take more than one format character at time. In Perl, there’s yet another mini-language (like regex, and what used to be called the format statement) for these builtins, and that’s not quite done yet.

But you can still take the entire header we’ve collected so far, and write it into a single unpack() statement like so:

my ( $header, $large-size, $small-size, $num-bdbs, $start-block ) =
  $file.read( 20 ).unpack( "A8 vv VV" );

This format is of course much more compact and much easier to read. In all probability once I get done with the main module I’ll convert everything over to this style and the code will become much, much quieter. Binary protocols, especially those for moisture evaporators, tend to have lots of code that looks like:

my $rev = $file.read(2).unpack("v")
if $rev == 0x01 {
  $r2 = $file.read(2).unpack("v");
} else {
  $d2 = $file.read(4).unpack("V");
}

where the next bytes you read depend upon the version of the protocol. Even though I’ve just been rattling off code based on the Perl version, I don’t know what the protocol may do at any given point. So it makes sense to read just one int or long ahead while developing.

I could read a version number as “V” because they started out using “v1”, “v2” and so on up to “v42792643522”. But then 30 lines and 2 revs later they may have changed from “V” to “vcc” because they wanted to support “v2.1.0” style.

And if that header were something like “A8 V CC* V vv” I have to go back and break up the format string and statement at the very least. If I go term-by-term I just have to find the version number and add an if-then statement just below.

Now that you’ve got a fairly good grounding in unpack(), I think it’s time for break. Next time we’ll cover writing our file back out, the most fun part of the operation.


Again, many thanks to those of you that have read this far. As usual, Gentle Reader, please feel free to leave constructive questions, comments, critiques and improvements in the comment section. I do require an email address for validation, but I don’t use it for any other purpose. Thank you again, and I’ll see you in part VI of this series.

Rewriting Perl Code for Raku IV: A New Hope

Back in Part III of our series on Raku programming, we talked about some of the basics of OO programming. This time we’ll talk about another aspect of OO programming. Perl objects can be made from any kind of reference, although the most common is a hash. I think Raku objects can do the same, but in this article we’ll just talk about hash-style Perl objects.

Raku objects let you superclass and subclass them, instantiate them, run methods on them, and store data in them. In previous articles we’ve talked about all but storing data. It’s time to remedy that, and talk about attributes.

Instance attributes

We used unit class OLE::Storage_Lite; to declare our class, and method save( $x, $y ) { ... } to create methods. Or in our case rewrite existing functions into methods. Now, we focus our attention on some of the variables that should really be instance attributes, and why.

Let’s get to know which variables behave like attributes, and which don’t. This will change how we write our Raku code, but hopefully for the better. We’ll start from the outside in, and look at the API. There are a few “test” scripts that use the module, and this fragment is pretty common.

use OLE::Storage_Lite;
my $oOl = OLE::Storage_Lite->new('test.xls');
my $oPps = $oOl->getPpsTree(1);
die( "test.xls must be a OLE file") unless($oPps);

The author creates an object ($oOl) from an existing file, then fetches a tree of “Pps” objects, whatever they are. So, one OLE::Storage_Lite object equals one file. This gives me my first instance variable, the filename.

sub new($$) {
  my($sClass, $sFile) = @_;
  my $oThis = {
    _FILE => $sFile,
  };
  bless $oThis;
  return $oThis;
}

Above is how they wrote it in Perl, and below is how we’d write it (exactly as specified) in Raku:

has $._FILE;

multi method new( $sFile ) {
  self.new( _FILE => $sFile );
}

Later on, we can call my $file = OLE:Storage_Lite.new( 'test.xls' ); just like we did in Perl. We wouldn’t even need the new method if we had users call my $file = OLE::Storage_Lite.new( _FILE => 'text.xls' );. This gives users the option of calling the API in the old Perl fashion or the new Raku fashion without additional work on our part.

Strict Raku-style

There’s a problem lurking here, though. The constructor Raku provides us lets us call my $file = OLE::Storage_Lite.new(); without specifying a value for $._FILE. If you know Perl’s Moose module, though, the ‘has’ there just might look familiar.

And for good reason. A lot of the ideas from Moose migrated into Raku during its design, and the attributes were one of those. Moose lets you do a lot of things with attributes, and so does Raku. One of those is you can add “adverbs” to them. Let’s do that now.

has $._FILE is required;

Calling OLE::Storage_Lite.new() now fails, because you’re not passing in the _FILE argument. That solves one problem. Actually, it solves two, come to think of it. In the original Perl code, you could call OLE:Storage_Lite->new() too, and it wouldn’t complain. Now we’ve fixed that, with one new term.

Progressive Typing

No, we’re not talking about some new editor like Comma (the link does work, despite the certificate problem.) Our code would run just fine, as-is. Users could call our .new() API, Raku would make sure the filename existed, and we could go on with translating.

But there’s something more we can take advantage of here, and that is the fact that any Raku object (and anything we can instantiate is an object) is a type as well. We haven’t mentioned that because we really couldn’t use that information until now.

The original Perl code is littered with clues to types, hidden in the variable names. When we wrote our own API call, the Perl code called the file name $sNm. The ‘s’ tells the Perl compiler nothing, but it tells us that $sNm is a String type. Perl may not have true types, but Raku does. Let’s fix our attribute with that in mind.

has Str $._FILE is required;

We knew all along that $._FILE is a string of some sort, but telling Raku that lets it allocate space more efficiently. Making sure it’s a required attribute lets anyone that calls new() know if they forget an argument. We could go a little farther with this, but locking down attributes will help in the long run, when we start dealing with the pack and unpack built-ins.

Packing It All In

We’re now getting to the heart of the module. There’s a lot of mechanics above us, allocating objects and doing math and checking types, and not much below us. The class’ entire purpose is to read and write OLE-formatted files. We’ll talk more about the boilerplate, but here’s the real meat of the file.

Let’s start with what should be simple, reading in data. Just like in Perl, we open a file and get back a “file handle” (assuming the file exists, of course.) By default, calling my $fh = open $._FILE; gives us a read-only file handle. The file handle itself has a bunch of attributes associated with it, but the important one right now is its encoding.

Namely, the fact that it has none. An OLE file is essentially a miniature filesystem (probably based on FAT) packed onto disk, complete with a root directory, subdirectories and files. File have names encoded in UCS-2, but the rest is entirely dependent upon what the application requires.

The upshot of which is that we can’t read the format with something simple like my @lines = $fh.lines; which would read line after line into the @lines array. Instead we’ll use calls like read() and write() that return byte-oriented buffers.

Buffering…

All OLE files start off with the header “\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1”, so we should probably start there. That’s important twice in the code, in fact. First, when we’re reading off disk, we can check it against what we’ve just read to make sure this file is OLE, and not, say, a JSON file. Later on, when we’re saving out an OLE file, we can write it as the header string.

constant HEADER-ID = "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1";

I’ll make it a constant as well, so when I revisit this code in a month I don’t have to go looking in specs for ‘0xd0 0xcf’ to remember what this is. Reading is straight-forward too. It needs just a byte count.

my Buf $header = $fh.read( 8 );

Something important to notice here is the type, ‘Buf’. If our file was in Markdown, or JSON we could get away with just writing my @lines = $fh.lines; like I tried earlier. But these are raw bytes, hindered by no interpretation. Let’s see what happens when we compare these bytes to our HEADER-ID.

t/01-internals.t ............ Cannot use a Buf as a string, but you called the Stringy method on it
  in method _getHeaderInfo at /home/jgoff/GitHub/drforr/raku-OLE-Storage_Lite/lib/OLE/Storage_Lite.pm6 (OLE::Storage_Lite) line 169
  in block <unit> at t/01-internals.t line 42

Another brick in the wall

Ka-blam. But… hold the phone here a minute, I just said $header eq HEADER-ID, I didn’t write anything like ‘Stringy’! There’s no ‘Stringy’ in the source… oh. HEADER-ID is a string, so Raku is being helpful. I’m trying to use string comparison (‘eq’) between something that’s not a Str ( $header ) and something that is (HEADER-ID).

Pull up the Stringy documentation, and look for the Type graph. Midway down you’ll see ‘Buf’ and ‘Str’, as of this writing Buf is on the left, and Str is popular so it’s in the middle.

Trace the inheritance paths from Buf and Str upwards, and you’ll see they pass Buf -> Blob -> Stringy and Str -> Stringy, and stop. What the error message therefore is saying is this, anthropomorphized:

You wanted to convert Buf to Str, and didn’t care how you did it. So I looked. First, on the Buf type. No .Str method there, at least without arguments. No good. So I looked in its parent, Blob. Nothing doing there. Then I looked at Stringy, and couldn’t find anything else.

There’s nothing above me, nothing below. So I’ll let you know I looked for a conversion method in a bunch of places, stopped at Stringy, and couldn’t go any farther. Sorry.

Raku

You’re probably wondering how to get out of this quandary. Reading the Blob documentation closely, you might think that the encode method is the way out of our present jam. If you look closer, though, there’s a spanner in the works. “\xD0” is the byte 0xd0, so if you try to decode to ASCII, you run into the problem that ASCII only covers 0x00-07xf, everything outside of that is undefined.

Packing for vacation

If you’ve kept up with things, you might surmise by now that the key to our quandary lies in the pack and unpack builtins. Specifically unpack(), because we’re trying to “decode” a buffer into something suitable for Raku.

Unless you’ve done things like network programming or security, the pack and unpack builtins are going to be unfamiliar territory. The closest analogue of pack() is the builtin sprintf().

Both of these builtins take a format string telling the compiler how to arrange its arguments. Both of them take a mixture of string and integer arguments afterwards. But while sprintf() takes the arguments and treats its output as a UTF-8 encoded string, pack() takes the same arguments and treats its output as a raw buffer of bytes.

And now you can see one way out of our little predicament. If we could just find the right invocation, pack() would be able to take our string “\xd0\xcf…” and turn it into a Buf object. Then we could compare the buffer we got by reading 8 bytes to the buffer we expected.

So instead of cluttering up the main code, let’s write a quick test.

use experimental :pack;
constant HEADER-ID = "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1";

use Test;
my $fh = open "test.xls";
my Buf $buf = $fh.read( 8 );

is $buf, pack( "A8", HEADER-ID ); # Pack 8 ASCII characters

Testing…testing…

Let’s take it from the top. We tell Raku to use the “experimental” pack() builtin, and declare the header we want to check against. Then we tell Raku we want to use the Test module, and open a new Microsoft Excel test file.

Last, we read a chunk of 8 bytes from the file into a buffer, and check to see that the 8 bytes matches the header we expect to see. Now, how did we get that weird ‘A8’ string in there? I thought pack() looked more like sprintf()?

Well, it does, to an extent. I/O routines like sscanf() and sprintf() can do all sorts of things to your strings and numbers on the way in and out, think for example what ‘%-2.10f’ means in a format specifier, for instance. You can follow along with the unpack() documentation if you like.

pack(), by contrast, just takes 8, 16, or 32-bit chunks of your input, and places them into a buffer. The “A” in “A8” says that it wants to convert an ASCII-sized chunk of your input (“\xd0” in our case) into a byte in the buffer, so our Buf now looks like ( 0xd0 ).

I could just as well have said “AAAAAAAA” in order to translate all 8 characters of the buffer, but I think it’s a little tidier to use the ‘repeat’ option, and say “A8” in order to convert just 8 characters (yes, yes, I know, they’re glyphs, but let’s not confuse matters.)

I could write “A*” just as well, but “A8” makes sure that 8 and only 8 (the number that thou shalt count to…) characters get converted. I doubt that the header in an OLE file will change, but it’s a nice bit of forward planning.


For those of you that made it this far, thank you. As usual, gentle Reader, if you have any comments, criticisms (constructive, please) or questions, feel free to post them below.

Next week I’ll delve deeper into the mysteries of pack(), unpack() and some of the tips and tricks I use to keep on my toes and make sure that I generate clean Microsoft-compatible output.

Rewriting Perl Code for Raku III: The Sorceror

Last week, we started testing, learned how to create proper Raku classes, and the basics of functions. This time we’ll take a closer look at functions, arguments, and make some decisions about the API. And maybe while writing this I’ll argue myself out of a decision. It’s happened before.

One good thing about writing about a module is that you can slip into a certain mindset. For instance, right now I’m thinking a few paragraphs ahead, wondering how to explain why I changed the API from Perl 5 references to regular Raku types.

It’s at odds with some of the principles I laid down at the start, which states that I should have minimal changes in the API from Perl to Raku. In Perl 5, you would create the “filesystem root” object like so:

my $root = OLE::Storage_Lite::PPS::Root->new(
  [ 0, 0, 0, 25, 1, 100 ],
  [ 0, 0, 0, 25, 1, 100 ],
  [ $workbook, $page_1, $sheet_1 ]
);

with a bunch of references to lists. By all rights, and the principles I set up earlier, the Raku equivalent should be almost exactly the same:

my $root = OLE::Storage_Lite::PPS::Root.new(
  [ 0, 0, 0, 25, 1, 100 ],
  [ 0, 0, 0, 25, 1, 100 ],
  [ $workbook, $page_1, $sheet_1 ]
);

In fact, all I did was copy and change two characters, specifically the Perl ‘->’ to the Raku ‘.’ operator. Clean, and very simple. And I think what I’ll do is actually just change the code back to using the Perl reference, at least in the API. Dereferencing it will be just a few lines, and I’ll have to change it in the tests as well, but I think the pain will be worthwhile.

This way I don’t have to field questions like “Why did you end up potentially breaking old code?” during talks. See, speaking at conferences about your code really can be a useful motivator!

I’d like a formal argument, please

So, I think I’ve settled on Perl-style formal references, at least for the current iteration. There are actually better ways to do this, but I’ll leave that for the proper Raku version. For right now, quick-n-dirty is the name of the game.

Moving on, we see an important method in the original Perl code, saving an object to disk.

sub save($$;$$) {
  my($oThis, $sFile, $bNoAs, $rhInfo) = @_;
  #0.Initial Setting for saving
  $rhInfo = {} unless($rhInfo);
  # ..
}

As I’ve mentioned before, OLE::Storage_Lite has been around for a long, long time. And it’s obvious here. Function prototypes (not signatures, which are a different kettle of fish) and the use of ‘$oThis’ instead of the more conventional ‘$self’.

Being prototypical

Prototypes were originally meant as a way to save you from having to write checks in your code. Theoretically, if your function was called sub save($$) and you tried to call it with save($fh) you would get an error, because the ‘$$’ means the subroutine took two arguments, and you gave it just one.

But it also predated objects (yes, Virginia, objects in Perl haven’t been around all that long.) and they could have unforeseen side effects. So they were a fad for a while, but quickly faded out of existence.

These days they’re a reason for a more experienced Perl hacker to take the junior aside and explain quietly why we don’t use those anymore, and point them to some modern references, like Modern Perl (not an affiliate link, yet.)

Let’s at least partially convert that to Raku, like so:

method save($sFile, $bNoAs, $rhInfo) {
  #0.Initial Setting for saving
  $rhInfo = {} unless($rhInfo);
  # ..
}

The ‘$oThis’ means that this is a method call, so instead of writing sub save( $oThis, ... ) we can rewrite it to a method and gain ‘self’ instead of the arbitrary variable ‘$oThis’. Of course we do have to do a search-and-replace on ‘$oThis’ with ‘self’, but that’s relatively simple. More complex is what to do with the ‘;’ in the original prototype.

Having options

It’s worth pointing out that OLE::Storage_Lite is taken at least in part from another (larger) module, OLE::Storage. This means that the internal code is redundant in a few places. Raku would let us rewrite what we have as:

method save($sFile, $bNoAs, $rhInfo = {}) {
  #0.Initial Setting for saving
  # ..
}

making $rhInfo an optional variable with a default value. Now, this is a pretty common pattern for a recursive method, so I did a bit of digging. Namely I grep’ed for ‘save’ in the original (all-in-one) Storage_Lite.pm module, and found no recursive calls to it.

Debugging both sides now

This is also where the test suite I wrote earlier comes in handy, as it actually exercises the ‘save’ method. So I added a quick debugging message warn "Saving $rhInfo"; to my local copy of the code, and ran the test suite. Seeing just one ‘Saving …’ message in my test output convinced me it wasn’t recursive. So now the code just looks like:

method save($sFile, $bNoAs) {
  #0.Initial Setting for saving
  my %hInfo;
  # ..
}

Also, since $rhInfo is created in this method, there’s no reason to leave it as a reference. So the initial ‘r’ goes away, and we have left just ‘%hInfo’. It may get passed in to other methods, but Raku lets us pass hashes and arrays as ordinary variable types, so I’ll take advantage of that.

To be fair, leaving it as a reference would have saved me a bit of typing, but I’d already kind of decided that at least internally I’d try to use Raku types and calling conventions, and that left me with the choice of how to pass variables around.

Having options

Finally, there’s the question of what to do with the semicolon. Remember at the start, the function prototype was ‘($$;$$)’ which meant $oThis and $sFile were before the semicolon, and $bData and $rhInfo were after. I can now reveal that ‘;’ in a Perl prototype means that whatever appears afterward is optional.

True to Raku’s nature, I can account for this in at least two ways. One way would be to decide that $bData is always there and just has a default value, probably 0. That would look like method save( $sFile, $bData = 0 ). But the documentation puts $bData in square brackets, indicating that it’s optional.

Raku has an alternate syntax to indicate if a variable is optional, which looks like method save( $sFile, $bData? ). I think this method is better than the alternative syntax because it states clearly that $bData is optional. Both methods work, I just happen to like the ‘?’ modifier.

Waiting for Huffman

Moving on, we have this wonderful line of code:

$rhInfo->{_BIG_BLOCK_SIZE}  = 2**
              (($rhInfo->{_BIG_BLOCK_SIZE})?
                  _adjust2($rhInfo->{_BIG_BLOCK_SIZE})  : 9);

When I was translating this initially, I was in something of a drone mindset, not truly thinking about what I was doing. I’d copied the $rhInfo variable into the method signature and just kept on writing. I ended up with a statement that I eventually shortened quite a bit.

$rhInfo.<_BIG_BLOCK_SIZE> = 2**
  ( $rhInfo.<_BIG_BLOCK_SIZE> ??
    _adjust2( $rhInfo.<_BIG_BLOCK_SIZE> ) !!
                                        9 );

The ‘.’ after $rhInfo indicates we’re dealing with a reference, and the <..> notation is now how barewords look inside hashes. The old {_BIG_BLOCK_SIZE} is still there, but it’s pronounced {‘_BIG_BLOCK_SIZE’}. A lot of people use the {‘..’} in Perl already so it’s not a big change, and it actually simplifies the backend enormously.

Also, at the start Larry and Damian pulled statistics on Perl code from CPAN and other repositories. They were looking for operator frequencies, among other things. Frequently used operators like qw() and -> got even shorter in Raku.

Others, like the ternary operator, weren’t so lucky. It got longer, and stretched to ‘?? .. !!’. So this is one place where the code will look a little funky. Maybe one day I’ll write a slang to fix it, but back to work.

Trimming the verge

Earlier I mentioned that this module was trimmed down from a much larger full OLE reader/writer. This was the first place that became evident. Since $rhInfo is now called %hInfo and initialized inside the method, this statement deserves to be looked at a little closer.

my %hInfo;
%hInfo<_BIG_BLOCK_SIZE> = 2**
  ( %hInfo<_BIG_BLOCK_SIZE> ??
    _adjust2( %hInfo<_BIG_BLOCK_SIZE> ) !!
                                        9 );

After replacing $rhInfo with %hInfo this is what I got. But since %hInfo is defined just above, the test %hInfo<_BIG_BLOCK_SIZE> will never be true, so this entire block can be reduced to:

my %hInfo = _BIG_BLOCK_SIZE => 2**9;

While I’m here I’ll delete _adjust2(). No code pathway uses it, so out it goes. I’ll restore it if I have to, but right now I want the test scripts to pass, and that’s it. I’ve got the original source, and a map from Perl to Raku, and that’s all I need.

Culling yaks from the herd

Where there’s smoke there’s fire, so I stop what I’m doing and grep out every ‘sub X’ call in the source, putting it in a scratch monkey. Then I go through the source (which I have below the new Raku source, deleting lines as I go) and look for methods that aren’t used, like adjust2(). I delete each of these methods with extreme prejudice, because each line of code I don’t see is one I don’t have to translate.

Checkpoint in git, and now it’s time for a lunch break. Afterwards, I’m getting into the save() method, and see what looks like a new yak to shave. Or a package to translate, to be precise.

  if(ref($sFile) eq 'SCALAR') {
    require IO::Scalar;
    my $oIo = new IO::Scalar $sFile, O_WRONLY;
    $rhInfo->{_FILEH_} = $oIo;
    # ...
}

In both Raku and Perl, you can create a single method called new( $sFile ) that treats $sFile as either a filename (scalar), file content (scalar reference) or file handle (scalar object.) In Perl, if we wanted to handle filenames, file contents, or file handles differently, we’d have to switch like this, or have different method names.

In Raku, we can handle this differently. In fact I can write the code to save() to a filename, and add save() to a filehandle later with no modifications needed. Above, I briefly touched on the fact that you can write more than one new() method, as long as the two method signatures were distinct.

multi method save( Str $filename ) {...}
multi method save( IO::Handle $fh ) {...}

Raku will let you write two methods called save(), as long as it can tell which one to call at runtime. So, I can call $root.save( '/tmp/test.xlsx' ); or $root.save( $out_filehandle ); and Raku will “dispatch” it to the right save() method automatically.

We call it ‘multiple dispatch’ for just that reason, dispatching a function call to multiple versions of a method. And this means that I can write the first save( Str $filename ) method without worrying about the other methods. I don’t have to add a new if-then branch to the existing code, or modify save() in any way.

I can just write my save() method and ignore the other IO:: types. Also, if someone gets my code later and wants to add a save() method that saves to something I know nothing about, they can write their new save() method without interfering with mine.

In this installment we’ve covered the basics of function and method calls, delved into the ternary operator, removed dead code and learned a little about multiple dispatch. Next time, we’ll open the binary filehandle we created above and delve into the mysteries of pack() and unpack().

I’ll also show you a new (yes, I couldn’t resist) grammar-based version of pack() that should cover the entire Perl gamut of packed types, with a bit of patience and a large enough test suite.

As always, gentle Reader, thank you for your time and attention. If you have any (constructive, please) comments, criticisms or questions, please let me know in the comment section below.