Beefy Boxes and Bandwidth Generously Provided by pair Networks
Welcome to the Monastery
 
PerlMonks  

An example of programming by contract

by princepawn (Parson)
on Nov 25, 2001 at 22:35 UTC ( [id://127400]=perlmeditation: print w/replies, xml ) Need Help??

In "Object-Oriented software construction" Meyer compares defensive programming with design by contract. This is a recent node on the same topic. You may have missed it because it did not use a descriptive title. The factorial function in the following program is written in design by contract fashion as it does not validate its input.

I seek two things in posting this

  • I want to know what you think about the quality of this code (ie, is it robust)
  • I want some input on how to get my validate function to return both a false value and also set an error string. Right now the line
    $! = "$_ validation failed";
    is not returning a printable error message.
    =head1 METHODS =head2 factorial INPUT : a non-negative integer, n OUTPUT: the factorial of n =cut sub factorial { my $number = shift; return 1 if $number <= 1; return ($number * factorial($number-1)) ; } sub validate_non_negative { $_[0] >= 0 } sub validate_integer { 1 } # how do you do this? sub validate { my $input = shift; my %arg = @_; for (@{$arg{as}}) { my $func = "validate_$_"; my $r = eval "$func($input)"; unless ($r) { $! = "$_ validation failed"; return 0; } } warn "validated $input"; return 1; } sub get { print "input? "; $_[0] = <>; } my $input; get($input) and (validate($input, as => [qw(non_negative integer)]) or die "error: + $!") and print factorial($input);
  • Replies are listed 'Best First'.
    Re: An example of programming by contract
    by chromatic (Archbishop) on Nov 26, 2001 at 00:23 UTC
      This would seem to be a good chance to use exceptions.

      Stylistically, I'd probably use sub refs instead of the string eval, too. If your pre- and post- conditions are in a package, UNIVERSAL::can comes to the rescue. Otherwise, this is an opportunity for a little symbol table muckery:

      sub validate { my $input = shift; my %arg = @_; for (@{$arg{as}}) { my $func = "validate_$_"; my $package = __PACKAGE__ . '::'; no strict 'refs'; if (exists ${ $package }{$func}) { $func = *{ ${ $package }{$func} }{CODE}; return unless $func->($input); } else { # throw exception and/or return } } warn "validated $input"; return 1; }
      Or you could build a hashref of validators, or cache them instead of explicitly going to the symbol table, or use the Listener pattern, or do all sorts of things.
    Re (tilly) 1: An example of programming by contract
    by tilly (Archbishop) on Nov 26, 2001 at 01:36 UTC
      As pointed out by chromatic, error conditions are generally best handled by throwing exceptions, and not by returning codes and assuming that your calling code is going to always write the necessary wrapper to notice the error conditions.

      As an example of the programmer errors that the latter approach lends itself to, look in your own code to your use of string evals without any sanity checks for whether $@ is being set. As a general rule string evals are to be avoided as long as you have any other sane (or saner) alternatives. That is not to say that they are bad, just that they are very undisciplined as control structures. Furthermore using manually synchronized naming schemes as a control pattern lends itself to its own problems. You can use a manually syncrhonized naming pattern to do the same things you can do with real anonymous data structures. But the latter is much faster, more efficient, and saner to work with.

      So to answer your second question, this is not the kind of code that I would like to see in production. And the main thing that I would cite is that it is trying to be too clever while ignoring basic principles of sane coding. Make sure that you have checked error conditions and will get debuggable information if things go wrong. Use real data structures where appropriate. If there exists a language feature (eg die/eval pairs) to do what you want, use it.

      Sorry.

    Re: An example of programming by contract
    by premchai21 (Curate) on Nov 25, 2001 at 22:44 UTC

      In perlvar, it is seen, $! is $OS_ERROR, that is, an error from the OS. I presume that your program is not the OS... you can't set $! AFAIK. I presume that you want to have error messages other than the standard ones from the OS, which you can't get from $! AFAIK. There is a way to set $@ to whatever you wish, though: by dieing. Other than that, I see no other way to signal errors, other than maybe making the return value an object which stringifies and numifies to itself, but which boolifies to false... perhaps using "Falsify" scalar strings?

      Update: Thanks tilly. Apparently my tests hadn't covered every case. They had, however, covered the case in question.

        It is indeed possible to set $! in user code.

        perl -e 'print ("$_ gives ", ($!=$_), "\n") for 1..20'
        However you will note that its behaviour is highly magical and (unless you do some magic of your own) not particularly usable for any purpose other than its intended.
          Thats kind of cool....
          122 gives Disk quota exceeded 123 gives No medium found 124 gives Wrong medium type 125 gives Unknown error 125 126 gives Unknown error 126 127 gives Unknown error 127
          Think a patch to set error 125 to 'Just another Perl hacker' would be frowned upon... ;-)

          -Blake

    Re: An example of programming by contract
    by blakem (Monsignor) on Nov 26, 2001 at 07:45 UTC
      function to return both a false value and also set an error string
      I hate to add a second semi-serious node to this thread but that reminds me of Fastolfe rather clever attempt to solve the same problem. He essentially creates strings that evaluate to false in boolean context... So, you can return false-with-an-error-message all in one scalar. Even the author doesn't recommend it for production use (since false-strings are non-intuitive to the sane perl programmer) but it is still an interesting approach.

      -Blake

    Re: An example of programming by contract
    by qslack (Scribe) on Nov 26, 2001 at 10:12 UTC
      Well, in response to
      sub validate_integer      { 1 } # how do you do this?

      ...you could do...
      sub validate_integer {$_[0] == int($_[0])}

      I assume, of course, that by "validate integer" you meant to check if the number was an integer.

      Quinn Slack
      perl -e 's ssfhjok qupyrqs&&tr sfjkohpyuqrspitnrapj"hs&&eval&&s&&&'
        Good try, but:
        die "Gotcha!" if validate_integer("not an integer");
        Personally I either wouldn't have the test, or else I would make the test be an RE pattern. eg:
        sub is_integer { $_[0] =~ /^-?\d+\z/; }
    Programming by Contract Example. revised
    by princepawn (Parson) on Nov 26, 2001 at 23:07 UTC
      This example takes the advice of tilly and chromatic with respect to how to write validation routines in a saner fashion.
      =head1 METHODS =head2 factorial INPUT : a non-negative integer, n OUTPUT: the factorial of n =cut sub factorial { my $number = shift; return 1 if $number <= 1; return ($number * factorial($number-1)) ; } our %validate = ( non_negative => sub { $_[0] >= 0 }, integer => sub { 1 } ); sub validate { my $input = shift; my %arg = @_; for (@{$arg{as}}) { my $func = $validate{$_}; my $r = $func->($input); die "$_ validation failed" unless $r ; } return 1; } sub get { print "input? "; $_[0] = <>; } my $input; get($input) and validate($input, as => [qw(non_negative integer)]) and print factorial($input);
        Currently, as it stands, this system will trigger a warning if the $input isn't a number of some sort. That's because of the >= in the non_negative validation.

        Also, what if I did something like

        validate($input, as => ['blah']);
        Since $validate{'blah'} doesn't exist, you end up with a fatal error. A simple (if uninformative) change would be to
        for (@{$args{as}}) { my $func = $validate{$_} || next; my $r = $func->($input); die "$_ validation failed" unless $r; }
        Better would be to throw some exception or warning.

        Furthermore, you do know that this will be less efficient, execution-wise, than just putting the checks into the program. This methdology is best if you plan on doing the same checks on a large variety of inputs. (Of course, you're just playing, which is cool.)

        One possibility is to, instead of forcing the user to validate the input, have the function validate its own input using these functions. Or, maybe a wrapper around factorial(). That way, everything is encapsulated.

        ------
        We are the carpenters and bricklayers of the Information Age.

        Don't go borrowing trouble. For programmers, this means Worry only about what you need to implement.

    Log In?
    Username:
    Password:

    What's my password?
    Create A New User
    Domain Nodelet?
    Node Status?
    node history
    Node Type: perlmeditation [id://127400]
    Approved by root
    help
    Chatterbox?
    and the web crawler heard nothing...

    How do I use this?Last hourOther CB clients
    Other Users?
    Others chanting in the Monastery: (6)
    As of 2024-03-28 16:51 GMT
    Sections?
    Information?
    Find Nodes?
    Leftovers?
      Voting Booth?

      No recent polls found