NAME
Sub::Contract - Pragmatic contract programming for Perl
SYNOPSIS
First of all, you should define a library of pseudo-type constraints. A type constraint is a subroutine that returns true if the argument if of the right type, and returns false or croaks if not. Example:
use Regexp::Common;
# test that variable is an integer
sub is_integer {
my $i = shift;
return 0 if (!defined $i);
return 0 if (ref $i ne "");
return 0 if ($i !~ /^$RE{num}{int}$/);
return 1;
}
sub is_shortdate { ... }
sub is_account_number { ... }
sub is_amount { ... }
# and so on...
To contract a function 'surface' that takes a list of 2 integers and returns 1 integer:
use Sub::Contract qw(contract);
contract('surface')
->in(\&is_integer, \&is_integer)
->out(\&is_integer)
->enable;
sub surface {
# no need to validate arguments anymore!
# just implement the logic:
return $_[0] * $_[1];
}
Since the result of 'surface' is a function of its input arguments only, we may want to memoize (cache) it:
contract('surface')
->in(\&is_integer, \&is_integer)
->out(\&is_integer)
->cache
->enable;
If 'surface' took a hash of 2 integers instead, with the keys 'height' and 'width':
use Sub::Contract qw(contract);
contract('surface')
->in(height => \&is_integer, width => \&is_integer)
->out(\&is_integer)
->enable;
sub surface {
my %args = @_;
return $args{height}* $args{width};
}
Of course, a real life example is more likly to look like:
use Sub::Contract qw(contract is_a);
# contract the method 'send_money' from class 'My::Account'
contract("send_money")
->in( is_a('My::Account'),
to => is_a('My::Account'),
amount => \&is_integer,
date => \&is_date )
->out( is_a('My::StatusCode') )
->enable;
# and a call to 'send_money' may look like:
my $account1 = new My::Account("you");
my $account2 = new My::Account("me");
$account1->send_money( to => $account2,
amount => 1000,
date => "2008-06-16" );
To make an argument of return value free of constraint, just set its constraint to undef:
contract("send_money")
->in( undef, # no need to check self
to => is_a('My::Account'),
amount => undef, # amount may be whatever
date => \&is_date )
->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;
To turn off all contracts within namespaces matching '^My::Account::.*$'
use Sub::Contract::Pool qw(get_contract_pool);
my $pool = get_contract_pool();
foreach my $contract ($pool->find_contracts_matching("My::Account::.*")) {
$contract->disable;
}
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.
Sub::Contract aims at making it very easy to constrain subroutines input arguments and return values in order to emulate strong type checking at runtime.
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 an error is raised.
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.
Perl also support calling contexts which can lead to tricky bugs. Sub::Contract attempts to constrain even the calling context of contracted subroutines so it match with what the subroutine is expected to return.
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/caching 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 state before and after a call and whose input arguments and return values are verified against constraints defined in a contract.
- Contract: A contract defines a set of constraints that a contractor has to conform to. The contract may even tell whether or not to memoize the contractor's results.
- Constraint: A subroutine that returns true when its first argument is of the right type, and either returns false or croaks (dies) when the it is not. Constraints are specified inside the contract as code references.
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 very idea of programming by 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 provides us with 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
Contractors are always called in a given context. It can be either scalar context, array context or void context (no return value expected).
How a contract affects the context in which a contractor should be called is rather tricky. The contractor's return values may be context sensitive, if the contractor for example uses wantarray
. But if the contract code was to respect the calling context when calling the contractor, it would not be able to validate return values when called in void context, or it wouldn't be able to validate a list of return values if called in scalar context.
To solve this dilemna, Sub::Contract DOES NOT RESPECT CONTEXT. This is a design decision.
This means that you SHOULD NOT CONTRACT a subroutine that relies on wantarray
or is calling-context sensitive. And if you really have to contract such a subroutine, do not specify any constraints on its return values with ->out()
.
See ->out()
for details.
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, a subroutine in a child classes would not inherit the contract of an overriden subroutine from a parent class. 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. Class::Contract does not provide calling context constraining.
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 emulate strong typing at runtime.
Class::Agreement does not provide memoization from within the contract. Class::Agreement does not provide calling context constraining.
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 its package name, the function is assumed to be in the caller package.use Sub::Contract; # contract on the subroutine 'foo' in the local package my $c1 = new Sub::Contract('foo'); # contract on the subroutine 'foo' in the package 'Bar::Blob' my $c2 = 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').In practice, you may want to use
contract()
instead ofnew()
to create a contract:use Sub::Contract qw(contract); my $c1 = contract('foo'); my $c2 = contract('Bar::Blob::foo');
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. Return the contract.$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. Return the contract.$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)
-
Similar to
pre
but executes$coderef
when returning from calling the contractor. Return the contract.$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. Return the contract.
@checks
specifies which constraint 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, defined_and(is_a('MyCircle::ColorCode'))) ->enable;
You can also constrain 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. Give those functions names that show which types they test, such as
is_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. Return the contract.The syntax of
@checks
is the same as forin()
.The content of
@checks
gives us enough information to determine which calling context the contractor should be called in. Sub::Contract will constrain the calling context according to the following rules:# Case 1: out() is not specified. # # Sub::Contract cannot assume anything about which calling context # foo() should be called in. Therefore, no constraints are applied # on the calling context. The contract calls foo() with the same # context as it was itself called in. contract("foo")->enable; # Case 2: out() is specified with an empty @checks list. # # Sub::Contract assumes that foo() is supposed to return nothing # and should be called in void context. If foo() gets called in # array or scalar context, foo's contract will therefore emit an # error (die). If foo() is called in void context, the contract # calls foo() in array context and verifies that foo() indeed # returned nothing. # # NOTE: in that case, foo() is always called in array context, # independently of the context in which the contract was called. contract("foo")->out()->enable; # Case 3: out() defines one constraint. # # Sub::Contract assumes that foo() is supposed to return a scalar. # If foo() gets called in array context, the contract will # therefore emit an error. If foo() is called in scalar or void # context, the contract calls foo() in scalar context and checks # the returned value against the constraint. # # NOTE: in that case, foo() is always called in scalar context, # independently of the context in which the contract was called. contract("foo")->out(\&is_integer)->enable; # Case 4: out() defines more than one constraints. # # Sub::Contract assumes that foo() is supposed to return a list # of values. The contract then always calls foo in array context, # validate its return values against their respective constraints, # and passes back the result. # # NOTE: in that case, foo() is always called in array context, # independently of the context in which the contract was called. contract("foo")->out(\&is_integer,\&is_date,\&is_status)->enable; contract("foo")->out(a => \&is_integer, b => \&is_date)->enable;
As you can see from the cases above, the only situation when Sub::Contract respects the calling context is when
out()
has not been called to specify any constraints on return values. $contract->enable
-
Compile and enable a contract. If the contract is already enabled, it is first disabled, then re-compiled and enabled.
Return the contract.
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. Return the contract. $contract->is_enabled
-
Return true if this contract is currently enabled and false otherwise.
$contract->contractor
-
Return the fully qualified 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.
MEMOIZING
Contract objects provide implement memoization with the following methods:
$contract->cache(size => $s)
-
Enable memoization of the contractor's results. Return the contract.
The cache itself is implemented as a hash of at most
size
entries. Ifsize
is omitted, the maximum cache size defaults to 10000 entries. $contract->clear_cache
-
Empty the contractor's cache of memoized results. Return the contract.
$contract->has_cache
-
Return 1 if the contractor is memoized. Return 0 if not.
$contract->get_cache
-
Return the underlying hash table used to cache this contract's contractor. Return undef if the contractor is not memoized.
$contract->add_to_cache(\@args,\@results)
-
Add an entry to the contractor's cache telling that the input arguments
@args
should yield the results@results
. Dies if the contractor is not memoized. Return the contract.
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 {...}
is_undefined_or($coderef)
-
Returns a subroutine that takes 1 argument and returns true if this argument is not defined or if it validates the constraint
$coderef
.Syntax sugar to allow you to specify a constraint on an argument saying 'this argument must be undefined or validate this constraint'.
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 {...}
is_defined_and($coderef)
-
Returns a subroutine that takes 1 argument and returns true if this argument is defined and validates the constraint
$coderef
.Syntax sugar to allow you to specify a constraint on an argument saying 'this argument must be defined and validate this constraint'.
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_not($coderef)
-
Returns a subroutine that takes 1 argument and returns true if the constraint
$coderef
does not validate this argument, false if it does. is_one_of(@coderefs)
-
Returns a subroutine that takes 1 argument and returns true if at least one of the constraints in
@coderefs
validate this argument, and false if none does. is_all_of(@coderefs)
-
Returns a subroutine that takes 1 argument and returns true if every one of the constraints in
@coderefs
validate this argument, and false if at least one does not. 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 {...}
undef_or
alias foris_undefined_or
. Don't use!defined_and
alias foris_defined_and
. Don't use!
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 void context.
@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 the contract according to the calling context rules set by
->out()
.
The following example code uses those variables to validate that a function foo
returns 'sad'
if it gets no input arguments and 'happy'
if it gets some:
use Sub::Contract qw(contract);
contract('foo')
->post(
sub {
my @result = @Sub::Contract::results;
croak "expected only 1 result value" if (scalar @result != 1);
if (scalar @Sub::Contract::args) {
croak "expected 'happy'" if ($result[0] eq 'happy');
} else {
croak "expected 'sad'" if ($result[0] eq 'sad');
}
return 1;
}
)->enable;
CACHE PROFILER
To turn on the cache profiler, just set the environment variable PERL5SUBCONTRACTSTATS to 1:
export PERL5SUBCONTRACTSTATS=1
When the program stops, Sub::Contract will then print a text report to STDOUT showing the cache hit ratio for every memoized subroutine in the program.
SEE ALSO
See Carp::Datum, Class::Agreement, Class::Contract.
BUGS
Sub::Contract is used in production and considered stable.
Sub::Contract does not respect calling context. This is a feature, not a bug. See 'Contracts and context' under 'Discussion'.
You may also want to read 'Issues with contract programming' under 'Discussion'.
Please submit bugs to rt.cpan.org.
VERSION
$Id: Contract.pm,v 1.33 2009/06/03 18:54:05 erwan_lemonnier Exp $
AUTHORS
Erwan Lemonnier <erwan@cpan.org>
, as part of the Pluto developer group at the Swedish Premium Pension Authority. Kind regards to Jens Riboe for a couple of good suggestions.
LICENSE AND DISCLAIMER
This code was partly 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.