Sunday, April 26, 2015

Please ignore, just testing styles

Writing Tests

I received some good feedback for my previous post Saving Vertical Space so I'd like to incorporate some of the suggestions in subsequent posts. Before I implement any of the code changes, the suggestion I should implement first is ... *drumroll* ... TESTING!

As some of you mentioned, I should already have had tests written to verify that the original function performed as expected, so that I can be confident that after I make changes to the function that I haven't introduced a bug.

If you build a module using using h2xs, you get a nifty test setup for free.

$ h2xs -AX Roller
Defaulting to backwards compatibility with perl 5.20.1
If you intend this module to be compatible with earlier perl versions, please
specify a minimum perl version with the -b option.

Writing Roller/lib/Roller.pm
Writing Roller/Makefile.PL
Writing Roller/README
Writing Roller/t/Roller.t
Writing Roller/Changes
Writing Roller/MANIFEST

If I make my Makefile

$ perl Makefile.PL
Checking if your kit is complete...
Looks good
Generating a Unix-style Makefile
Writing Makefile for Roller
Writing MYMETA.yml and MYMETA.json

I can run the default tests:

$ make test
cp lib/Roller.pm blib/lib/Roller.pm
PERL_DL_NONLAZY=1 /home/dave/perl5/perlbrew/perls/perl-5.20.1/bin/perl "-MExtUtils::Command::MM" "-MTest::Harness" "-e" "undef *Test::Harness::Switches; test_harness(0, 'blib/lib', 'blib/arch')" t/*.t
t/Roller.t .. ok
All tests successful.
Files=1, Tests=1,  0 wallclock secs ( 0.01 usr  0.01 sys +  0.02 cusr  0.00 csys =  0.04 CPU)
Result: PASS

There's not much in the default test file, since we haven't done any coding yet:

$ cat t/Roller.t
# Before 'make install' is performed this script should be runnable with
# 'make test'. After 'make install' it should work as 'perl Roller.t'

#########################

# change 'tests => 1' to 'tests => last_test_to_print';

use strict;
use warnings;

use Test::More tests => 1;
BEGIN { use_ok('Roller') };

#########################

# Insert your test code below, the Test::More module is use()ed here so read
# its man page ( perldoc Test::More ) for help writing this test script.

I cleaned up lib/Roller.pm, completed the basic POD and added the roll() function.

  1. package Roller;
  2. use strict;
  3. use warnings;
  4. require Exporter;
  5. our @ISA = qw(Exporter);
  6. our %EXPORT_TAGS = ( 'all' => [ qw(roll) ] );
  7. our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
  8. our @EXPORT = qw( );
  9. our $VERSION = '0.01';
  10. sub roll {
  11. my $input = shift;
  12. return unless $input =~ /(\d*)d(\d+)\s*(\D?)\s*(\d*)/;
  13. my $num = $1 || 1;
  14. my ($die,$plus,$end) = ($2,$3,$4);
  15. my $total = 0;
  16. my @dice;
  17. push @dice, int(rand($die))+1 for ( 1..$num );
  18. if ( $plus eq 'b' ) {
  19. $end = $num if $end > $num;
  20. @dice = sort { $b <=> $a } @dice;
  21. $#dice = $end-1;
  22. }
  23. $total += $_ for @dice;
  24. if ( $plus eq '+' ) { $total += $end }
  25. elsif ( $plus eq '-' ) { $total -= $end }
  26. elsif ( $plus eq '*' ) { $total *= $end }
  27. elsif ( $plus eq '/' ) { $total /= $end }
  28. return $total;
  29. }
  30. 1;
  31. __END__
  32. =head1 NAME
  33. Roller - Perl extension for generating dice rolls from a "dice language"
  34. string such as "3d6+1".
  35. =head1 SYNOPSIS
  36. use Roller ':all';
  37. print roll('d20');
  38. =head1 DESCRIPTION
  39. Roller's single function C<roll()> generates dice rolls from a "dice language"
  40. string such as "3d6+1".
  41. =head2 EXPORT
  42. None by default.
  43. =head1 AUTHOR
  44. David M. Bradford, E<lt>davembradford@gmail.comE<gt>
  45. =head1 COPYRIGHT AND LICENSE
  46. Copyright (C) 2015 by David M. Bradford
  47. This library is free software; you can redistribute it and/or modify
  48. it under the same terms as Perl itself, either Perl version 5.20.1 or,
  49. at your option, any later version of Perl 5 you may have available.
  50. =cut

I also renamed t/Roller.t to t/01-Roller.t and copied it to t/02-rolls.t to put new tests in. Now how do we test against random numbers? Here was my first stab at it:

  1. use strict;
  2. use warnings;
  3. use Test::More tests => 50;
  4. use Roller ':all';
  5. for ( 1..50 ) {
  6. my $roll = roll('1d4');
  7. ok( ($roll >= 1 and $roll <= 4), 'roll() is valid');
  8. }

I realized looking at this that I could be much more specific:

  1. use strict;
  2. use warnings;
  3. use Test::More tests => 50;
  4. use Roller ':all';
  5. for ( 1..50 ) {
  6. my $roll = roll('1d4');
  7. ok( $roll == 1
  8. || $roll == 2
  9. || $roll == 3
  10. || $roll == 4, 'roll() is valid');
  11. }

TODO

  • minor code change
  • devel::cover
  • pic
  • other considerations

DONE

  • feedback

Sunday, April 05, 2015

Saving Vertical Space

I was reviewing some code I had written for a simple RPG dice algorithm (although there's already a good module for this, Game::Dice) and I realized again that I have a prefererence for functions that can fit on one screen. One strategy is breaking up the code into smaller routines but I sometimes like to compact it vertically as much as possible first.

This function roll, given a string of "dice language," should return the results of such a dice roll. An example of this would be "3d10+1" to roll three 10-sided dice and then add 1, or "4d6b3" which says to roll four 6-sided dice and take the best three.

Here's the function before the refactor:

sub roll {
    my $input = shift;
    die unless $input =~ /d/;
    if ( $input =~ /(\d*)d(\d+)\s*(\D?)\s*(\d*)/ ) {
        my $num   = $1 || 1;
        my $die   = $2;
        my $plus  = $3;
        my $end   = $4;
        my $total = 0;
        my @dice;
        for my $count ( 1 .. $num ) {
            my $single = int( rand($die) ) + 1;
            push @dice, $single;
            print "$single\n";
        }
        if ( $plus eq 'b' ) {
            if ( $end > $num ) {
                $end = $num;
            }
            @dice = sort { $b <=> $a } @dice;
            $#dice = $end - 1;
        }
        for my $die (@dice) {
            $total += $die;
        }
        if ( $plus eq '+' ) {
            $total += $end;
        }
        elsif ( $plus eq '-' ) {
            $total -= $end;
        }
        elsif ( $plus eq '*' ) {
            $total *= $end;
        }
        elsif ( $plus eq '/' ) {
            $total /= $end;
        }
        return $total;
    }
    return;
}

The first thing I did is to delete the first of this pair of lines, which was redundant, because the line that follows also checks the format of the input:

die unless $input =~ /d/;
if ( $input =~ /(\d*)d(\d+)\s*(\D?)\s*(\d*)/ ) {

But instead of having that big if block, I changed it to this:

return unless $input =~ /(\d*)d(\d+)\s*(\D?)\s*(\d*)/;

Then I combined these:

my $die   = $2;
my $plus  = $3;
my $end   = $4;

into this:

my ($die,$plus,$end) = ($2,$3,$4);

Once I decided I didn't need to print each individual die as it was rolled, I could reduce this:

for my $count ( 1 .. $num ) {
    my $single = int( rand($die) ) + 1;
    push @dice, $single;
    print "$single\n";
}

to this:

push @dice, int(rand($die))+1 for ( 1..$num );

Then, I changed this:

if ( $end > $num ) {
    $end = $num;
}

To use the postfix if:

$end =  $num if $end > $num;

and this:

for my $die (@dice) {
    $total += $die;
}

to use postfix for:

$total += $_ for @dice;

One thing I like to do with an if/else chain like this:

if ( $plus eq '+' ) {
    $total += $end;
}
elsif ( $plus eq '-' ) {
    $total -= $end;
}
elsif ( $plus eq '*' ) {
    $total *= $end;
}
elsif ( $plus eq '/' ) {
    $total /= $end;
}

is to compress it like this:

if    ( $plus eq '+' ) { $total += $end }
elsif ( $plus eq '-' ) { $total -= $end }
elsif ( $plus eq '*' ) { $total *= $end }
elsif ( $plus eq '/' ) { $total /= $end }

Since it's still short in width and the syntax can lined up to be quite readable.

So the final version of the refactored function is:

sub roll {
    my $input = shift;
    return unless $input =~ /(\d*)d(\d+)\s*(\D?)\s*(\d*)/;
    my $num = $1 || 1;
    my ($die,$plus,$end) = ($2,$3,$4);
    my $total = 0;
    my @dice;
    push @dice, int(rand($die))+1 for ( 1..$num );
    if ( $plus eq 'b' ) {
        $end =  $num if $end > $num;
        @dice = sort { $b <=> $a } @dice;
        $#dice = $end-1;
    }
    $total += $_ for @dice;
    if    ( $plus eq '+' ) { $total += $end }
    elsif ( $plus eq '-' ) { $total -= $end }
    elsif ( $plus eq '*' ) { $total *= $end }
    elsif ( $plus eq '/' ) { $total /= $end }
    return $total;
}

Now you can make things a lot smaller (see Perl Golf examples) but readability is important to me, and I think this is arguably as readable as the original. I was actually a little surprised that perltidy barely touched the if/elsif structure, just screwing up the alignment a little on the first line:

if ( $plus eq '+' ) { $total += $end }
elsif ( $plus eq '-' ) { $total -= $end }
elsif ( $plus eq '*' ) { $total *= $end }
elsif ( $plus eq '/' ) { $total /= $end }

The code doesn't strictly adhere to Perl Best Practices, which is something I like to use as a guide for the most part, but perlcritic (which is based on Perl Best Practices) doesn't start to complain until the cruel setting, then bringing up things like postfix if, postfix for, and unless.

How would you make it smaller while still maintaining readability?

Sunday, March 15, 2015

My Bad Communication Skills

A couple weeks ago I asked how you "join the conversation" but based on feedback I got, I don't think I communicated well. I think people thought I meant "which blogs do you read?" What I really meant was: when you write a blog entry, where do you post the link so that it's seen by people who are interested in that subject?

So for example, when I write about Perl, I post to blogs.perl.org. I want to blog about other topics too, like web development (JavaScript, CSS, etc); lifehacks; Unix, Linux, shell scripting; general tech / tech business; and database stuff. But when I blog I'd like it to have a chance of being seen. Please let me know your thoughts!

Saturday, February 21, 2015

How do you join the conversation?


blogs.perl.org is great in that it's a stream of blog posts around a specific technology. Since I, like many of you, blog about other technologies too, I'd like to learn from you about other conversation streams. For me personally, the list of topics include:

  • Web development (JavaScript, CSS, etc)
  • Lifehacks
  • Unix, Linux, shell scripting
  • General tech / tech business
  • Database

I'll add what little knowledge I have on the topic:

There's reddit with corresponding subreddits on Unix and Perl. It looks like it's pretty routine to post blog entries on the Perl one at least, I'm not sure about the climate of other tech subreddits.

There are also subreddits for LifeProTips and Life Hacks. I've not yet participated in either, so again I'm not sure what the expectations are.

The Unix and Linux Forums, while not a "stream" of conversation, seems to be receptive to sharing ideas, and the people there are friendly and helpful.

And of course, Twitter with and without #perl and #unix hashtags. I say with or without as it seems like more popular bloggers, at least, don't bother with the hashtags. I'll also say that with or without the hashtags I haven't seen a lot of traffic coming from Twitter.

I'd appreciate anyone's advice, experience, or knowledge on this topic.

Thanks!

Tuesday, February 10, 2015

Fix Those Legacy Subroutines or Methods


Maybe you know the feeling… you go to add an option to that method or subroutine and… cue Jaws theme

sub update_shopping_cart {
    my $cart_id        = shift;
    my $item           = shift;
    my $quantity       = shift;

Argh. You don’t want your legacy code to break but you also don’t want to add a fourth unnamed parameter to the existing problem. And the solution is simple:

sub update_shopping_cart {
    my $cart_id        = shift;
    my $item           = shift;
    my $quantity       = shift;
    my $apply_discount = 0;        # Initialize the fourth parameter

    my $param1 = $cart_id;
    if ( ref $param1 eq 'HASH' ) {
        $cart_id        = $param1->{cart_id};
        $item           = $param1->{item};
        $quantity       = $param1->{quantity};
        $apply_discount = $param1->{apply_discount};
    }

Now either of these work. The legacy call:

update_shopping_cart( 314, 'apples', 3 );

…or the new style:

update_shopping_cart({
    cart_id        => 314,
    item           => 'apples',
    quantity       => 3,
    apply_discount => 1,
});

Bonus: there is no way to use the new option with the old-style call. If someone wants to use it, they’ll need to switch to the new style.

Pros:
  • The new call is self-documenting. In the original form of the call, you see “314” in the code by itself, and it’s not immediately obvious what it is. Now it’s nicely labeled.
  • Now that you have added the new format, you can painlessly add additional named parameters as needed.

Cons:
  • It may be confusing to see two different styles of calls in your codebase. But, now you can transition the old code piecemeal.

Saturday, January 31, 2015

Command Line Project Manager (clpm) v1.0.1 released


clpm is designed to make managing sets of files easier at the Unix command line. It's free and open source. Please view the demo here. Find more info at http://tinypig.com/clpm

Sunday, January 18, 2015

Call for help with open source project "CLPM"



CLPM is my “Command Line Project Manager”. It’s a tool I wrote and have been using myself for several years now, and I am releasing it in the hope that others might find it useful.

Also, if you have been looking for an open source project to contribute to, here’s your chance! I don’t care what your level of experience is, if you think you have a useful comment or contribution, I’d like to hear from you!

There is a Todo section in the README, but I want to add a couple notes here:
  1. It’s currently not packaged, nor does it have an installer. This probably makes it much less likely to be adopted.
  2. I’m not sure how to promote it to make sure its audience (developers/sysadmins maybe) at least get a chance to see it, even if it ends up that it’s useful to nobody but me.
The project page has more details: https://github.com/tinypigdotcom/clpm