NAME
Sub::Contract - Pragmatic contract programming for Perl
SYNOPSIS
To contract a function 'divid' that accepts a hash of 2 integer values and returns a list of 2 integer values:
contract('divid')
->in(a => sub { defined $_ && $_ =~ /^\d+$/},
b => sub { defined $_ && $_ =~ /^\d+$/},
)
->out(sub { defined $_ && $_ =~ /^\d+$/},
sub { defined $_ && $_ =~ /^\d+$/},
)
->enable;
sub divid {
my %args = @_;
return ( int($args{a} / $args{b}), $args{a} % $args{b} );
}
Or, if you have a function is_integer
:
contract('divid')
->in(a => \&is_integer,
b => \&is_integer,
)
->out(\&is_integer, \&is_integer);
->enable;
If divid
was a method of an instance of 'Maths::Integer':
contract('divid')
->in(sub { defined $_ && ref $_ eq 'Maths::Integer' },
a => \&is_integer,
b => \&is_integer,
)
->out(\&is_integer, \&is_integer);
->enable;
Or if you don't want to do any check on the type of self:
contract('divid')
->in(undef,
a => \&is_integer,
b => \&is_integer,
)
->out(\&is_integer, \&is_integer);
->enable;
You can also declare invariants, pre- and post-conditions as in usual contract programming implementations:
contract('foo')
->pre( \&validate_state_before )
->post( \&validate_state_after )
->invariant( \&validate_state )
->enable;
You may memoize a function's results, using its contract:
contract('foo')->memoize->enable;
You may list contracts during runtime, modify them and recompile them dynamically, or just turn them off. See 'Sub::Contract::Pool' for details.
DESCRIPTION
Sub::Contract offers a pragmatic way to implement parts of the programming by contract paradigm in Perl.
Sub::Contract is not a design-by-contract framework.
Perl is a weakly typed language in which variables have a dynamic content at runtime. A feature often wished for in such circumstances is a way to define constraints on a subroutine's arguments and on its return values. A constraint is basically a test that the specific argument has to pass otherwise we croak.
For example, a subroutine add()
that takes 2 integers and return their sum could have constraints on both input arguments and on the return value stating that they must be defined and be integers or else we croak. That kind of tests is usually writen explicitely within the subroutine's body, hence leading to an overflow of argument validation code. With Sub::Contract you can move this code outside the subroutine body in a relatively simple and elegant way. The code for add
and its contract could look like:
contract('add')
->in(\&is_integer,\&is_integer)
->out(\&is_integer)
->enable;
sub add { return $_[0]+$_[1] }
Sub::Contract doesn't aim at implementing all the properties of contract programming, but focuses on some that have proven handy in practice and tries to do it with a simple syntax.
With Sub::Contract you can specify a contract per subroutine (or method). A contract is a set of constraints on the subroutine's input arguments, its returned values, or on a state before and after being called. If one of these constraints gets broken at runtime, the contract fails and a runtime error (die or croak) is emitted.
Contracts generated by Sub::Contract are objects. Any contract can be disabled, modified, recompiled and re-enabled at runtime.
All new contracts are automatically added to a contract pool. The contract pool can be searched at runtime for contracts matching some conditions.
A compiled contract takes the form of an anonymous subroutine wrapped around the contracted subroutine. Since it is a very appropriate place to perform memoization of the contracted subroutine's result, contracts also offer memoizing as an option.
There may be only one contract per subroutine. To modify a subroutine's contract, you need to get the contract object for this subroutine and alter it. You can fetch the contract by querying the contract pool (see Sub::Contract::Pool).
The contract definition API is designed pragmatically. Experience shows that contracts in Perl are mostly used to enforce some form of argument type validation, hence compensating for Perl's lack of strong typing, or to replace some assertion code.
In some cases, one may want to enable contracts during development, but disable them in production to meet speed requirements (though this is not encouraged). That is easily done with the contract pool.
DISCUSSION
Definitions
To make things easier to describe, let's agree on the meaning of the following terms:
- Contractor: The contractor is a subroutine whose pre- and post-call state and input and return arguments are verified against constraints defined in a contract.
- Contract: Defines a set of constraints that a contractor has to conform with and eventually memoizes the contractor's results.
- Constraint: A test that returns true when the constraint passes, or either returns false or croaks (dies) when the constraint fails. Constraints are specified inside the contract as code references to some test code.
Contracts as objects
Sub::Contract differs from traditional contract programming frameworks in that it implements contracts as objects that can be dynamically altered during runtime. The idea of altering a contract during runtime may seem to conflict with the definition of a contract, but it makes sense when considering that Perl being a dynamic language, all code can change its behaviour during runtime.
Furthermore, the availability of all contracts via the contract pool at runtime gives a powerfull self introspection mechanism.
Error messages
When a call to a contractor breaks the contract, the constraint code will return false or croak. If it returns false, Sub::Contract will emit an error looking as if the contractor croaked.
Contracts and context
In Perl, contractors are always called in a given context. It can be either scalar context, array context or no context (no return value expected).
How this affects a contractor's contract is rather tricky. The contractor's return values may be context sensitive. Therefore, the following choices were made when designing Sub::Contract:
If a subroutine usually returns a scalar or an array but is called in void context, the part of the contract that validates return values will not see any return values from the subroutine. This implies that calling in void context a subroutine whose contract has constraints on the return values will be seen as a contract breach.
If a subroutine returns an array but is called in scalar context, the part of the contract that validates return values will see only 1 return value from this subroutine: an integer telling the number of elements in the returned array. This implies that calling in scalar context a subroutine whose contract has constraints on a list of return values will be seen as a contract breach.
Issues with contract programming
- Inheritance
-
Contracts do not behave well with inheritance, mostly because there is no standard way of inheriting the parent class's contracts. In Sub::Contract, child classes do not inherit contracts, but any call to a contractor subroutine belonging to the parent class from within the child class is verified against the parent's contract.
- Relevant error messages
-
To be usable, contracts must be specific about what fails. Therefore it is prefered that constraints croak from within the contract and with a detailed error message, rather than just return false.
A failed constraint must cause an error that points to the line at which the contractor was called. This is the case if your constraints croak, but not if they die.
Other contract APIs in Perl
Sub::Contract VERSUS Class::Contract
Class::Contract implements contract programming in a way that is more faithfull to the original contract programming syntax defined by Eiffel. It also enables design-by-contract, meaning that your classes are implemented inside the contract, rather than having class implementation and contract definition as 2 distinct code areas.
Class::Contract does not provide memoization from within the contract.
Sub::Contract VERSUS Class::Agreement
Class::Agreement offers the same functionality as Sub::Contract, though with a somewhat heavier syntax if you are only seeking to validate input arguements and return values.
Class::Agreement does not provide memoization from within the contract.
TODO: more description TODO: how to enable contracts -> enable on each contract, or via the pool TODO: validation code should not change @_, else weird bugs...
Object API
my $contract = new Sub::Contract($qualified_name)
-
Return an empty contract for the function named
$qualified_name
.If
$qualified_name
is a function name without the package it is in, the function is assumed to be in the caller package.# contract on the subroutine 'foo' in the local package my $c = new Sub::Contract('foo'); # contract on the subroutine 'foo' in the package 'Bar::Blob' my $c = new Sub::Contract('Bar::Blob::foo');
A given function can be contracted only once. If you want to modify a function's contract after having enabled the contract, you can't just call
Sub::Contract-
new> again. Instead you must retrieve the contract object for this function, modify it and enable it anew. Retrieving the function's contract object can be done by querying the contract pool (See 'Sub::Contract::Pool'). my $contract = new Contract::Sub($name, caller => $package)
-
Same as above, excepts that the contractor is the function
$name
located in package$package
. $contract->invariant($coderef)
-
Execute
$coderef
both before and after calling the contractor.$coderef
gets in arguments the arguments passed to the contractor, both when called before and after calling the contractor.$coderef
should return 1 if the condition passes and 0 if it fails.$coderef
may croak, in which case the error will look as if caused by the calling code. Do notdie
from$coderef
, always usecroak
instead.package MyCircle; use accessors qw(pi); # define a contract on method perimeter that controls # that the object's attribute pi remains equal to 3.14 # before and after calling ->perimeter() contract('perimeter') ->invariant(sub { croak "pi has changed" if ($_[0]->pi != 3.14) }) ->enable; sub perimeter { ... }
$contract->pre($coderef)
-
Same as
invariant
but executes$coderef
only before calling the contractor.$coderef
gets in arguments the arguments passed to the contractor.$coderef
should return 1 if the condition passes and 0 if it fails.$coderef
may croak, in which case the error will look as if caused by the calling code. Do notdie
from$coderef
, always usecroak
instead. $contract->post($coderef)
-
Similaar to
pre
but executes$coderef
when returning from calling the contractor.$coderef
gets in arguments the return values from the contractor, eventually altered by the context (meaning()
if called in void context, a scalar if called in scalar context and a list if called in array context).$coderef
should return 1 if the condition passes and 0 if it fails.$coderef
may croak, in which case the error will look as if caused by the calling code. Do notdie
from$coderef
, always usecroak
instead. $contract->in(@checks)
-
Validate each input argument of the contractor one by one.
@checks
declares which validation functions should be called for each input argument. The syntax of@checks
supports arguments passed in array-style, hash-style or a mix of both.If the contractor expects a list of say 3 arguments, its contract's
in
should look like:contract('contractor') ->in(\&check_arg0, \&check_arg1, \&check_arg2)
Where
check_argX
is a code reference to a subroutine that takes the corresponding argument as input value and returns true if the argument is ok, and either returns false or croaks if the argument is not ok.If some arguments need not to be checked, just replace the code ref of their corresponding constraint with
undef
:# check input argument 0 and 2, but not the middle one contract('contractor') ->in(\&check_arg0, undef, \&check_arg2)
This comes in handy when contracting an object method where the first passed argument is the object itself and need not being checked:
# method perimeter on obect MyCircle expects no # arguments, but method color expects a color code contract('perimeter')->in(undef)->enable; contract('color') ->in(undef, sub { return defined $_[0] && ref $_[0] eq 'MyCircle::ColorCode'}) ->enable;
You can also constraint arguments passed in hash-style, and it look like this:
# function add expects a hash with 2 keys 'a' and 'b' # having values that are integers contract('add') ->in(a => \&is_integer, b => \&is_integer) ->enable;
If
add
was a method on an object,in()
would look like:contract('add') ->in(undef, a => \&is_integer, b => \&is_integer) ->enable;
Finally, you can mix list- and hash-style argument passing. Say that
add()
expects first 2 arguments then a hash of 2 keys with 2 values, and all must be integers:contract('add') ->in(\&is_integer, \&is_integer, a => \&is_integer, b => \&is_integer) ->enable;
Most of the constraints on arguments will in fact act like type constraints and be the same all across your contracts. Instead of declaring again and again the same anonymous sub in every contract, create a function that tests this specific type, such as
is_integer
. Give those functions names that show which types they test, such asis_integer
,is_string
,is_date
,is_arrayref
and so on. It is also a good idea to gather all those functions in one specific module to import together withSub::Contract
.If you don't want to check whether the argument is defined or not in every constraint, you may want to use
defined_and
andundef_or
(see further down). $contract->out(@checks)
-
Same as
in
but for validating return arguments one by one.out()
validates return values in a context sensitive way. See 'Contract and context' under 'Discussion' for details.The syntax of
@checks
is the same as forin()
. $contract->memoize
-
Enable memoization of the contractor's results.
TODO: detail arguments
$contract->flush_cache
-
Empty the contractor's cache of memoized results.
$contract->enable
-
Compile and enable a contract. If the contract is already enabled, it is first disabled, then re-compiled and enabled.
Enabling the contract consists in dynamically generating some code that validates the contract before and after calls to the contractor and wrapping this code around the contractor.
$contract->disable
-
Disable the contract: remove the wrapper code generated and added by
enable
from around the contractor. $contract->is_enabled
-
Return true if this contract is currently enabled.
$contract->contractor
-
Return the fully qualified name name of the subroutine affected by this contract.
$contract->contractor_cref
-
Return a code reference to the contracted subroutine.
$contract->reset
-
Remove all previously defined constraints from this contract and disable memoization.
reset
has no effect on the contract validation code as long as you don't callenable
afterreset
.reset
is usefull if you want to redefine a contract from scratch during runtime.
Class API
contract($qualified_name)
-
Same as
new Sub::Contract($qualified_name)
. Must be explicitly imported:use Sub::Contract qw(contract); contract('add_integers') ->in(\&is_integer, \&is_integer) ->enable; sub add_integers {...}
undef_or($coderef)
-
Syntax sugar to allow you to specify a constraint on an argument saying 'this argument must be undefined or validate this test'.
Assuming you have a test function
is_integer
that passes if its argument is an integer and croaks otherwise, you could write:use Sub::Contract qw(contract undef_or); # set_value takes only 1 argument that must be either # undefined or be validated by is_integer() contract('set_value') ->in(undef_or(\&is_integer)) ->enable; sub set_value {...}
defined_and($coderef)
-
Syntax sugar to allow you to specify a constraint on an argument saying 'this argument must be defined and validate this test'.
Example:
use Sub::Contract qw(contract defined_and undef_or); # set_name takes a hash that must contain a key 'name' # that must be defined and validate is_word(), and may # contain a key 'nickname' that can be either undefine # or must validate is_word(). contract('set_name') ->in( name => defined_and(\&is_word), nickname => undef_or(\&is_word) ) ->enable; sub set_name {...}
is_a($pkg)
-
Returns a subroutine that takes 1 argument and returns true if this argument is an instance of
$pkg
and false if not.Example:
# argument 'name' must be an instance of String::Name contract('set_name') ->in( name => is_a("String::Name") ) ->enable; sub set_name {...}
Class variables
The value of the following variables is set by Sub::Contract before executing any contract validation code. They are designed to be used inside the contract validation code and nowhere else!
$Sub::Contract::wantarray
-
1 if the contractor is called in array context, 0 if it is called in scalar context, and undef if called in no context. This affects the value of
Sub::Contract::results
. @Sub::Contract::args
-
The input arguments that the contractor is being called with.
@Sub::Contract::results
-
The result(s) returned by the contractor, as seen by its caller. Can also be accessed with the exported function 'returns'.
The following example code uses those variables to validate that a function foo
returns 'array'
in array context and 'scalar'
in scalar context:
use Sub::Contract qw(contract results);
contract('foo')
->post(
sub {
my @results = returns;
if ($Sub::Contract::wantarray == 1) {
return defined $results[0] && $results[0] eq "array";
} elsif ($Sub::Contract::wantarray == 0) {
return defined $results[0] && $results[0] eq "scalar";
} else {
return 1;
}
}
)->enable;
SEE ALSO
See Carp::Datum, Class::Agreement, Class::Contract.
BUGS
See 'Issues with contract programming' under 'Discussion'.
VERSION
$Id: Contract.pm,v 1.20 2008/05/22 16:08:56 erwan_lemonnier Exp $
AUTHORS
Erwan Lemonnier <erwan@cpan.org>
, as part of the Pluto developer group at the Swedish Premium Pension Authority.
LICENSE AND DISCLAIMER
This code was developed at the Swedish Premium Pension Authority as part of the Authority's software development activities. This code is distributed under the same terms as Perl itself. We encourage you to help us improving this code by sending feedback and bug reports to the author(s).
This code comes with no warranty. The Swedish Premium Pension Authority and the author(s) decline any responsibility regarding the possible use of this code or any consequence of its use.