Advent of Code, Intcode & subroutine signatures
As you hopefully are aware of, Advent of Code has started again on the 1st December. On day 2 you have to write an interpreter for something called Intcode (I won't repeat the specs here, so you might want to read the details on adventofcode.com). After solving the first part you get this hint:
"Keep [your code] nearby during this mission - you'll probably use it again. Real Intcode computers support many more features than your new one, but we'll let you know what they are as you need them."
So I packed my ugly-as-hell hacked together code I wrote in the morning and put it into a proper Perl module. To make things at least a bit interesting, I decided to use the new (still experimental) Signatures feature and some fancy postfix deref array slices (near the end of this post..)
Without further ado, the code
package Intcode;
use strict;
use warnings;
use 5.030;
use feature 'signatures';
no warnings 'experimental::signatures';
use base qw(Class::Accessor::Fast);
__PACKAGE__->mk_accessors(qw(pos code));
After the obvious package
declaration and using strict
& warnings
, I enable the signature feature
and disable the experimental::signature
warnings. I do hope that this boilerplate will be not necessary soon (maybe in 5.32?).
Instead of using a full object system like Moose, I just grab the old & battle-tested Class::Accessor::Fast and define two accessors, pos
(the current position in the list of codes) and code
(the actual input code).
sub new ($class, $code, $pos = 0) {
return bless {
code => $code,
pos => $pos,
}, $class;
}
I don't use the default constructor provided by Class::Accessor
, because I want to simplify constructing a new object using the following API:
my $ic = Intcode->new( [ 1,0,0,3,99 ] );
So I define three positional arguments: $class
, $code
and $pos
. As $pos
is defined with a default ($pos = 0
), it is interpreted as an optional argument, which makes for an even cleaner initialization (at least for now, let's see what future tasks will bring..)
Using $code
and $pos
, I create a Hashref
, which I bless
into the $class
(using very old-school Perl). The such created object is then returned to the caller.
Using Intcode
Here's the solution to the first part, using Intcode
:
use 5.030;
use strict;
use warnings;
use lib '.';
use Intcode;
my @code = split( ',', <STDIN> );
$code[1] = 12;
$code[2] = 2;
my $intcode = Intcode->new( [@code] );
say $intcode->runit;
After the boilerplate, I get the input from STDIN
and split it into an array. Then set (as per the instructions) the two first input values and generate a new Intcode
instance using the parsed code
. Now I only have to call runit
to get the result.
runit
sub runit ($self, $final = 0) {
while (1) {
my $op = $self->code->[ $self->pos ];
my $method = 'op_' . $op;
last unless defined $self->$method;
}
return $self->code->[$final];
}
runit
again uses signatures instead of manual @_
unpacking, though I wish there was method
keyword that would automatically set up $self
. $final
is another optional parameter defaulting to 0. It might be used in the future to find the position of the element to report back; on day 2 this always was the first element (i.e. 0
in computer count), so I consider this some future-proofing...
Inside the method I run an endless while
loop. I fetch the op code from the current position ($self->pos
), which in the first iteration is 0. I generate a method name base on the value I find at the position, eg op_1
or op_2
. Then I call this method. If it returns undef
(from op_99
) I exit the loop and return the value at the $final
position. If it returns something else, the loop continues.
@h4. op_99
sub op_99 {
return undef;
}
Now that's some boring code...
op_1 and op_2
sub op_1 ($self) {
my ( $x, $y, $t ) = $self->get_n( 3 );
$self->code->[$t] = $self->code->[$x] + $self->code->[$y];
}
sub op_2 ($self) {
my ( $x, $y, $t ) = $self->get_n( 3 );
$self->code->[$t] = $self->code->[$x] * $self->code->[$y];
}
Basically the same, the only difference is the mathematical operator + vs *. I use a helper method get_n
(more on that in a minute) to get the needed amount of codes (in this case, 3: the location of the two summands and the location of the target), and the assign the target ($t
) the result of the calculation (addition or multiplication) using the values stored at the respective locations. As Perl automatically returns the value of the last expression, the while
loop in runit
will continue to run.
get_n
sub get_n ($self, $n) {
my $pos = $self->pos;
my @pointer = $self->code->@[ $pos + 1 .. $pos + $n ];
$self->pos( $pos + $n + 1 );
return @pointer;
}
This method contains the only (at least slightly...) smart code. It gets the object $self
and a count $n
, again via subroutine signatures. The we get the current position $pos
. We now use another newish Perl feature postfix dereference combined with an array slice. Let's break this down a bit:
my @pointer = $self->code->@[ $pos + 1 .. $pos + $n ];
[ $pos + 1 .. $pos + $n ]
This defined a range starting at $pos + 1
going to $pos + n
. I set $n = 3
, so in the first loop (when $pos
is 0) we get: 0+1 .. 0+3
, i.e. 1, 2, 3
.
$self->code->@[ 1, 2, 3];
arrayref->@[ list ]
defines a slice from the the arrayref, i.e. we get back the list of values passed via the slice list.
So if $self->code
is 1,0,0,3,99
, then $self->code->@[1,2,3]
will return (0,0,3)
.
I assign this list to @pointer
.
But before returning @pointer
, we have to move the position forward the correct amount of steps. On day 2, we always have to skip 3 steps, but I assume that later op codes will have a different amount of arguments. So instead of hardcoding this, we move forward the number of elements we needed to get, plus one (because we want to get to the element after the op args.
And that's it!
Here is my solution for the second task, again using Intcode:
use 5.010;
use strict;
use warnings;
use lib '.';
use Intcode;
my @code = split( ',', <STDIN> );
for my $n ( 0 .. 99 ) {
for my $v ( 0 .. 99 ) {
$code[1] = $n;
$code[2] = $v;
my $intcode = Intcode->new( [@code] );
my $res = $intcode->runit;
if ( $res == 19690720 ) {
say "$n, $v -> " . ($n* 100 + $v);
exit;
}
}
}
Oh, and please give Advent Of Code a try - it' really fun!
P.S.: If you want to join our (Vienna (and Prague( Perl (and other language) hackers) leaderboard, contact me for the key!