magnify
Home coding Confident Code Using Type Constraints
formats

Confident Code Using Type Constraints

The other day on Hacker News, there was a good discussion of Avdi Grimm‘s talk on how to write Confident Code given at this year’s Ruby Midwest. He covers four areas: gather input, perform work, deliver results and handle failure. Here are links to the code example showing the initial, timid and final, confident code. I’ll just cover the input gathering step because I think embedded type constraints is vital for improving coding efficiency and minimizing boilerplate.

Most of this can actually be done using something like Ruby’s Doodle. It does not appear that Doodle is very popular yet (21 all time messages in the mailing list) though I think it would be good if it were more so.

More popular is Stevan Little‘s Moose for Perl which has 28 named contributors. While I like to read about many technologies I generally like to use more mature and popular technologies than less so I’ve been using Perl’s Moose and haven’t coded with Doodle yet.

To show how Moose is powerful, let’s write the Confident Code example in Perl

Confident Code in Moose

I whipped this up for discussion so it can definitely be refined, however, it should get the ideas across. Here are some things that are covered:

  1. Input Validation: Inputs are typed and given default values in both attributes (e.g. width as an Integer with default 40) and methods (e.g. messages as an ArrayRef of Strings – ArrayRef[Str])
  2. Separation of Concerns: attributes and methods are conveniently separated for easier maintenance and testing
  3. Output: IO::All is used for output, defaulting to STDOUT (i.e. ‘-’)
use MooseX::Declare;

class CowsayTheMooseway {
  use Capture::Tiny qw(capture_merged);
  use IO::All;
  use Log::Log4perl qw(:easy);
  Log::Log4perl->easy_init($INFO);

  has width       => ( isa => 'Int', is => 'rw', default => 40      );
  has eyes        => ( isa => 'Str', is => 'rw', default => ''      );
  has cowfile     => ( isa => 'Str', is => 'rw', default => 'moose' );
  has destination => ( isa => 'Str', is => 'rw', default => '-'     );

  method command {
    my $command = 'cowsay -W '           . $self->width;
    $command   .= ' -e '.$self->eyes    if $self->eyes;
    $command   .= ' -f '.$self->cowfile if $self->cowfile;
  }

  method say (ArrayRef[Str] $messages) {
    my $command = $self->command;
    my $output  = capture_merged {
      system "$command $_" for @$messages;
    };
    $output > io $self->destination;
    INFO "wrote to ".$self->destination;
  }
}

Note how the input attributes and methods are all separated cleanly with their own input types.

Here’s how to run the class:

use CowsayTheMooseway;

CowsayTheMooseway->new(
  destination => 'moo.txt'
)->say([
  'Psst...Want More Confidence?',
  'Here is How',
  'Moose This Way'
]);

And the results:

 ______________________________
< Psst...Want More Confidence? >
 ------------------------------
  \
   \   \_\_    _/_/
    \      \__/
           (oo)\_______
           (__)\       )\/\
               ||----w |
               ||     ||
 _____________
< Here is How >
 -------------
  \
   \   \_\_    _/_/
    \      \__/
           (oo)\_______
           (__)\       )\/\
               ||----w |
               ||     ||
 ________________
< Moose This Way >
 ----------------
  \
   \   \_\_    _/_/
    \      \__/
           (oo)\_______
           (__)\       )\/\
               ||----w |
               ||     ||

Let me know what you think. Should this type of coding be done in Ruby? Is Doodle the way to go or is there something else? Can the Perl code be improved?

 
 Share on Facebook Share on Twitter Share on Reddit Share on LinkedIn
3 Comments  comments 
  • Mcrose

    Since your attributes (‘eyes’, ‘cowfile’, etc) are read-write but used in the lazily generated command attribute, you need to define a clearer for the command and have the attributes used in its generation trigger the clearer when they’re set. Otherwise, code like `my $cow = CowsayTheMooseway->new(); $cow->eyes(‘XX’); $cow->say(‘Something’);` won’t work right.

    • Mcrose

      Ack, since I can’t edit; throw another $cow->say() between the ->new() and the ->eyes().

    • http://johnwang.com John Wang

      Good point. With command is as a lazy attribute, it is only generated once so it won’t get updated if the parameters are changed after it is called the first time. As you mention, this can be fixed with a clearer. Here’s what it would look like with the clearer.

      has command => (
      isa => ‘Str’,
      is => ‘ro’,
      lazy => 1,
      default => method {
      my $command = ‘cowsay -W ‘.$self->width;
      $command .= ‘ -e ‘.$self->eyes if $self->eyes;
      $command .= ‘ -f ‘.$self->cowfile if $self->cowfile;
      },
      clearer => ‘clear_command’
      );

      after eyes { $self->clear_command };
      after cowfile { $self->clear_command };
      after width { $self->clear_command };However, since I’m trying to showcase input validation in the article and not necessarily lazy attributes, I changed it to a simple method call that gets generated each time it’s called. I think this is fine here because command is generated by the class, is lightweight and isn’t a user input.