Perl flexibility for the win

Tags:

This week I spent some time on the Weekly Challenge 119 which is always a good opportunity to get some deliberate practice in.

This week I completed task one, "Swap Nibbles" in both Perl and GoLang.

Now I know I am biased and my main language is Perl, but this was definitely easier/faster than doing it in GoLang.

The point of the exercise is to take a decimal integer, convert it to the binary representation, flip the two "nibbles" and return the resulting decimal integer.

Even with my verbose style of coding these things my solution was this:


sub swap {
    my ( $self, $n ) = @_;

    my $oct = sprintf( "%08b", $n );

    $oct =~ /^(.{4})(.{4})$/;

    return oct "0b$2$1";
}

I streamed myself coding this up on the Perl.Kiwi Twitch Channel which is also on YouTube:

Perl's ability to allow me to take a variable $n and treat it like an integer, then the awesome power of Perl's regex engine makes it simple to swap the nibbles. Then in the return statement auto-magically takes the strings taken in the regex and combines them into another string and oct treats it like a number and converts it to decimal.

It's helpful in a way that non-Perl people can find disconcerting. It's hugely flexible and useful. It's also one of the reasons people hate Perl; it is super easy to write code that is buggy as things are "fluid".

My second attempt was in GoLang:


func NSwap(n int) int {
    bin := fmt.Sprintf("%08b", n)

    a := bin[0:4]
    b := bin[4:8]

    c := b + a

    d, _ := strconv.ParseInt(c, 2, 64)
    return int(d)
}

It's not too dissimilar to be honest, probably influenced by my Perl implementation (and certainly not idiomatic Go). It's not much more complex and follows a similar style of approach. However the conversion steps are more distinct as GoLang will not allow different types to interact in the way Perl does.

The second task was another example of how Perl's flexibility can make it very easy to solve problems quickly and easily. In this task, you need to create a sequence of numbers using just 1,2,3 and that don't have the number 1 next to itself (so 11 excluded, 211 excluded).

My implementation looks like this:


sub no_one_on_one {
    my ( $self, $n ) = @_;

    my @seq;

    my $x = 0;
    while (1) {
        $x++;
        next unless $x =~ /^[123]/;
        next if $x =~ /[4567890]/g;
        next if $x =~ /11/g;
        push @seq, $x;
        last if @seq > $n - 1;
    }
    return $seq[-1];
}

Again, Perl's regex engine for the win. And unless is super helpful here, it helps me read easily.

Rather than trying to create a sequence that obeys the rules; I just loop around incrementing $x by one each time. Then I skip to the next iteration if the $x if the number does not start with one of our numbers (1,2,3). We skip if any number other than 1,2,3 appears. Finally we skip if "11" appears.

If we get past the "next" tests, we push the number onto an array. If that array is long enough to match the limit the user inputs $n we exit the loop with last and return the final element in the array $seq[-1];

I should probably point out here that I am not criticising Go, it's a great language. But Perl is a great language too. And much of the time I prefer Perl over Go, Elm, Raku, PHP, etc. Why? Mainly familiarity if I am being honest with you. It's a really mature, battle tested language. Perl is a general purpose language designed for flexibility and expressiveness. It is not the fastest (or slowest); it has some weaknesses and strengths... as do all programming languages.

Perl is designed to be the "swiss-army chainsaw" a developer reach for when they have a problem to solve. Go on the other hand is design to create "simple, reliable and efficient software"

The next post might be on last weeks challenge, I did take a crack at task #2 and it was really interesting to resolve and to do in a specific way.

TDD a perlscript with modulinos (PWC-118)

Tags:

Perl is a language that has a strong testing culture, the CPAN Testing Service (CPANTS) for example is amazing. When you release any module it gets tested on a huge variety of systems and operating systems and Perl versions automatically and for free.

Perl also has great testing tools, Test:: family of modules are hard to beat.

Writing a new module in a Test Driven Development (TDD) style is very much possible and is my preferred way of tackling the Perl Weekly Challenge when I put time aside to take them on. What you can also do is TDD the script itself via a "modulino".

This week's challenge is a script to tell you if the number you provide is a binary palindrome, you'll probably want that context for the code examples that follow. :-)

To make this possible you can wrap all the code in your script in a sub (for example run()). then call that sub only when the code is being called from Perl, rather than from the test suite. But first we want to start with a test.

use Test::More;

require_ok('./ch-1.pl');

done_testing;

This uses the standard Test::More module for "require_ok" so we can load the script itself. This test will fail till you create the script itself.

As I am creating a script that outputs to the screen (stdout) I want to test the output of the command so I use the Test::Output module.

use Test::More;
use Test::Output;

require_ok('./ch-1.pl');

stdout_is { &run() }
    'foo',
    'Dumb test to see if the test works';

done_testing;

So then we can expand the script to look like this:

__PACKAGE__->run() unless caller;

sub run {
    print "foo";
}

1;

From here we can take small steps towards out goal whilst having the safety of tests. So the test might become more sensible like this:

use Test::More;
use Test::Output;

require_ok('./ch-1.pl');

stdout_is { &run(5) }
    'xxxxx',
    '5 is a palindrome';

done_testing;

This is a small test that confirms that the script gives the desired answer in the format we want. In this case I have already coded up the module in a TDD style; so this is really about testing the wiring up of the code. Version one might look like this:

__PACKAGE__->run() unless caller;

use lib './lib';
use Binary::Palindrome;

sub run {
    my $n = $ARGV[0] || shift;

    my $bp = Binary::Palindrome->new;

    print $bp->is_palindrome($n);
}

1;

So in this still more than I actually did in step one. I actually just added the use lines and ran the tests to see if everything stayed the same. I use tests that way a lot; confirming I have not added a typo or simple syntax error. I take small steps so spotting the simple mistakes is... well simple.

The my $n = $ARGV[0] || shift; line is a way of making the sub handle input from the command line (the first arg, aka $ARGV[0]) or a scalar from the tests via shift.

In the above situation the test should fail as I am looking for "xxxxx", but I know that the code "should" return 1. So the test fails and tells me I am ok. SO then I can change the test to:

stdout_is { &run(5) }
    '1',
    '5 is a palindrome';

And this should pass, next we want to confirm the real behaviour, so test becomes:

stdout_is { &run(5) }
'1 as binary representation of 5 is 101 which is Palindrome.',
    '5 is a palindrome';

This will fail of course, so lets change the script to pass this test (and forgive me I am skipping steps):

__PACKAGE__->run() unless caller;

use lib './lib';
use Binary::Palindrome;

sub run {
    my $n = $ARGV[0] || shift;

    my $bp = Binary::Palindrome->new;
    if ( my $res = $bp->is_palindrome($n) ) {
        print "$res as binary representation of $n is ",
            $bp->represent_as_binary($n), " which is Palindrome.";
    }
}

1;

This should pass, so lets add the next test:

stdout_is { &run(4) }
'0 as binary representation of 4 is 100 which is NOT Palindrome.',
    '4 is NOT a palindrome';

Which fails, so then we add the code:

    if ( my $res = $bp->is_palindrome($n) ) {
        print "$res as binary representation of $n is ",
            $bp->represent_as_binary($n), " which is Palindrome.";
    }
    else {
        print "$res as binary representation of $n is ",
            $bp->represent_as_binary($n), " which is NOT Palindrome.";
    }

Adding this else makes our test passes. So we are in a good place.

This is a slightly contrived example, and there are many improvements to make. However, I now have a solid base to work from. This is a style of working I quite like. Think of it perhaps as Behaviour Driven Development (BDD) as well as TDD. o Or perhaps this could be described as an "Integration test" or even an "End to End test" given what we are building. I am not pedantic about the terms of purity of my TDD/BDD.

Personally I am aiming for tools that make my development easier and more reliable and this sort of testing does that for me.

When I coded this up I did TDD the module itself, so the two methods (is_palindrome and represent_as_binary) have tests.

Not doing the script and it's tests first, has affected the design of the module. The script is more complicated and the code less efficient as I a using both methods in the script AND I use represent_as_binary in the is_palindrome method. If it had side effects (i.e. wrote to a file or DB) then this would be bad; or if it was slow.

The next step might be to create a third method, that returns a data structure with the result and the binary representation. This would be more efficient; I would probably leave the construction of the text in the script as it's the "presentation layer" and leaves the module a little cleaner; though your mileage may vary. A method that returned the full answer would be very tidy in the script (thin controller).

Please do checkout the full code in the Perl Weekly Challenge git repo (https://github.com/manwar/perlweeklychallenge-club/tree/master/challenge-118/lance-wicks/perl).

And if you have any questions please drop me a message.

Perl Weekly Challenge 109 in Perl and Elm

Tags:

This week I tried a new OBS setup to livecode the solution on Twitch.tv/lancew. The setup is thanks to Martin Wimpress who shared his setup ( Creating the Slim.AI OBS Studio set ) and this broadcast is extra special as I was able to bring Mohammad Anwar into the live stream; which was awesome.

This week I solved the "Chowla Numbers" task of the Perl Weekly Challenge, first in Perl; then in Elm.

Both I did as TDD-ish as I could, some cheating where I just Data::Dumper'd to STDOUT in the Perl version. The Elm one, I used the tests a bit more as I am less familiar in the language, and to be frank I didn't know an easy way to dump to the screen. ;-)

The Elm version I ended up with this:

module Chowla exposing (chowla, n, nums)

import List


nums : Int -> List Int
nums max =
    List.range 1 max
        |> List.map n


n : Int -> Int
n num =
    let
        denominators =
            List.range 2 (num - 1)

        numbers =
            List.map (chowla num) denominators
    in
    List.foldl (+) 0 numbers


chowla : Int -> Int -> Int
chowla num denom =
    case remainderBy denom num of
        0 ->
            denom

        _ ->
            0

The Perl looks like this:

package Chowla;

use Moo;

sub list {
    my ($self,$max) = @_;
    my @chowla_numbers;
    for my $n (1..$max) {
        push @chowla_numbers, $self->n($n);
    }
    return \@chowla_numbers;
}

sub n {
    my ($self,$num) = @_;

    my $total = 0;
    for my $n (2..$num-1) {
        if ($num % $n == 0) {
            $total += $n;
        }
    }
    return $total;
}

1;

Neither are particularly long, the Elm without type signatures is 15 lines. The Perl 21 lines (though if you formatted it differently it would be about the same).

The Elm code, not having a for loop relies on map and fold (specifically foldl). I could/should try the Perl with a map. There is no equivalent of foldl in Perl. foldl is in interesting command, basically like a map, but collapsing in on itself via a function, in my case + so just summing all the numbers.

Testing the code in Perl was as ever easy, Perl's Test2:V0 is a great tool. On the Elm side I am getting more comfortable with the testing, I did not use any of the built in fuzzing on this one, I will have to explore it more.

Over the next week I have some more Go code I want to work on for https:://fantasy-judo.com and I'd like to work on modernising my https://www.vwjl.net/ a little more, the combat simulation code is where my attention should go. Given that, I may not live stream the weekly challenge this week, but might stream some general hacking about.

Perl Weekly Challenge 107 - Self describing numbers... live coded

Tags:

Having an additional day off this week (extending the UK Easter break) I decided to do another short live coding session.

Once again the subject being the (Perl Weekly Challenge)[https://perlweeklychallenge.org/blog/perl-weekly-challenge-107/#TASK1] by Mohammad Anwar.

This weeks task #1 is to write a script that produces a list of the first three "self describing numbers". I tackled it without any prep (pre-reading or planning), which is always risky, but it went OK.

As ever I try and work in a Test Driven Development (TDD) style. This means I try and work out small steps with tests that confirm my hypothesis as I go along. I am not super strict about what that means. I use TDD as a tool to help me solve problems, rather than a strict approach. Especially on this sort of recreational coding; I don't worry at all about coverage or too much about being strict TDD... whatever your definition of strict TDD is.

I use warn and Data::Dumper too. The mindset is the same as TDD, I am testing the idea of what is next.

There has been a discussion on Twitter about TDD (as always) and I think I fall into the camp of thinking that TDD is about driving my design, not providing coverage of edge cases. That's a different issue. Unit tests may still be the tool, but the objective is different. If you watch the video you'll see I only really code the "happy path"; intentionally so. I am trying to determine the solution to the problem. So it's about interfaces and making it easy to make changes without regressing.

You'll see in the video that I end up in a muddle a couple of times. I notice that that's when I have not been using tests to guide me. I also notice that at the end I was easily able to demonstrate changing the if statement from eq to == and know immediately if it was OK as the tests proved it still worked. This is what TDD gives you. That ability to refactor (even if it is only going from eq to ==) with some confidence. The confidence encourages me/you to refactor more than as it's not scary.

Another observation I should point out (and I have done so before) is that I tend to start with minimal test and package... as it saves me from typo bugs that can be ever so frustrating. I try not to write a full set of tests of examples.

The last thing I will add, is that if you watch the video... you'll see I end up with I think 3 working solutions to the task requirement. Starting with simply returning an array of the three numbers expected with no real logic. My test proves that the call works. It also provides the foundation that the next piece of coding provided. The second solution was looping through all the numbers from 1 to 30,000 and checking (via a new sub) if that number was self-describing. Again, I did this the "cheats way" of looking for the specific three numbers and returning positively with three simple if statements.

Only then did I write a solution that actually worked across any number.

If this was a professional coding problem. I would ideally do it the same way. Why? Because I had a provably working solution I "could" ship very quickly. The later solutions were better and more in line with what would actually be wanted; but getting those first two solutions coded up and tested is important at it allowed me to learn quickly and in small increments and if I'd committed the code at the right points a colleague could have carried on the project if need be. Or having gotten it out the door fast; maybe it turns out we don't need that feature and we revert with minimal time invested in the feature.

In this example, it's not such a big deal. But I've been in the situation where the code changes are hundreds-thousands of lines of code for a new feature that we didn't know would work. And watching a huge piece of work fail and being cancelled is far more painful emotionally as a developer (and financially as a business) than a small change.

Thanks again to Mohammad for this awesome project of his!

Perl Weekly Challenge 106

Tags:

This week I quickly did the Perl Weekly Challenge as a live coding session on Twitch (now also on Youtube: https://www.youtube.com/watch?v=E4Bwh-FVTns ).

If you watch the video hopefully you'll see the approach I take to these challenges, I don't write defensively, I do write in a test first and test as you go basis. Below is the test file that i ended up with, it could be made better I nkow, but it got me to a working, tested solution nice and quickly.

# t/01-maxgap.t
use Test2::V0 -target => Maxgap;

my @example1 = (2, 9, 3, 5);
my $expected1 = 4;

is $CLASS->maxgap(@example1), $expected1, 'Example 1';

my @example2 = (1, 3, 8, 2, 0);
my $expected2 = 5;

is $CLASS->maxgap(@example2), $expected2, 'Example 2';


my @example3 = (5);
my $expected3 = 0;

is $CLASS->maxgap(@example3), $expected3, 'Example 3';

done_testing;

Using test2::V0 hides the horrible "stutter", by which I mean the module name and the method repeat: Maxgap::maxgap. So ideally I would change the names.

The code looks like this:

# lib/Maxgap.pm
package Maxgap;

sub maxgap {
    my ($self, @integers) = @_;

    @integers = sort @integers;

    my $maxgap = 0;
    for my $i (1 .. @integers-1){
        my $gap = $integers[$i] - $integers[$i-1];
        if ( $gap > $maxgap) {
            $maxgap = $gap;
        }
    }

    return $maxgap;
}

1;

It's not too complex, and I am sure with some refactoring it could be made more idiomatic. This version however works and was up and running in less than 30 minutes (of coding time).

The final step is to use it in a simple command line script:

# ch-1.pl
use strict;
use warnings;

use lib './lib';
use Maxgap;

my $maxgap = Maxgap::maxgap(@ARGV);

print "Output: $maxgap\n";

You can see the "stutter" is pretty pronounced here. There is zero error checking so that's not great.

Anyway... I am working on the Elm version and will share another day if I come up with a version I like.