blob: 95895e3dd34ab7485e364c14690d3b735c38fc0d [file] [log] [blame]
package CheckedClass; # XXX need a better class name
# StrongTyped, Declared, StaticTyped?
use Carp;
=head1 NAME
CheckedClass - do static type checking
=head1 SYNOPSIS
package YourClass;
@ISA = ('CheckedClass'); # Your class isa CheckedClass.
sub type_info { # Your class MUST define type_info.
return {
required_args => { # Caller MUST pass these to new().
ARGNAME => 'YourArgType'
},
optional_args => { # Caller MAY pass these to new().
OTHER => 'SCALAR'
},
members => { # YourClass MAY create these.
SOME_HASH => 'HASH'
}
};
}
# Inherit CheckedClass::new, which calls YourClass::initialize.
sub initialize {
my ($self) = @_;
$self->{SOME_HASH} = &whatever;
}
sub your_method {
my ($self) = @_;
$self->{SOME_HASH}->{some_key}++;
$self->check; # May typecheck %{$self} at any time.
}
package main;
$ya = bless {}, YourArgType;
$yc1 = YourClass->new(ARGNAME => $ya);
$yc2 = YourClass->new(ARGNAME => $ya, OTHER => 17);
=head1 DESCRIPTION
CheckedClass provides a way for a subclass to declare the types of its
member variables and constructor arguments and check those types at run
time.
A checked class must define the method B<type_info>, which must return
a hash with three entries: B<required_args>, B<optional_args>, and
B<members>. Each of the three entries is a hash whose keys are member
variables and whose values describe the types of the member variables.
CheckedClass provides a constructor, B<new>, that enforces the rules
in type_info. Callers of B<new> must provide all arguments listed in
required_args, as keyword arguments, with values of the type
specified. Callers of B<new> may also provide keyword arguments
listed in optional_args, which are also type checked. Any keywords
not listed or keywords with values of the wrong type will cause a
runtime error. B<new> creates the object as a hash reference and
inserts correct arguments into the hash. Then it calls the
B<initialize> method, which may be provided by the subclass.
CheckedClass also provides a B<check> member that may be called at
any time to verify that an object only has the permitted members and
that they have the permitted types.
CheckedClass supports multiple inheritance, but all of the ancestors
of a checked class must be derived from CheckedClass.
=cut
$CheckedClass::Skip_Checks = 0;
my %Type_Info_Cache;
sub CheckedClass::new {
my $template = shift;
my $classname = ref($template) || $template; # see perlobj(1) man page
my $self = do {
local $SIG{__WARN__} = sub { }; # ignore "Odd number of elements" msg
bless { @_ }, $classname;
};
$self->check_new();
$self->initialize();
$self->check();
return $self;
}
sub CheckedClass::initialize {
# placeholder, subclasses can override.
}
sub CheckedClass::check_new {
return if $CheckedClass::Skip_Checks;
my ($self) = @_;
my $classname = ref($self);
my $info = $self->merged_type_info();
for (@{$info->{required_args}}) {
croak "required arg \"$_\" missing in $classname constructor"
unless exists $self->{$_};
}
while (my ($arg, $val) = each %$self) {
my $expected_type = $info->{arg_types}->{$arg};
croak "unknown arg $arg in $classname constructor"
unless $expected_type;
my $actual_type = ref($val);
unless ($actual_type) {
$actual_type = \$val; # e.g., SCALAR(0x123).
$actual_type =~ s/\(.*//; # change "SCALAR(0x123)" to "SCALAR".
}
croak "arg $_ type mismatch in $classname constructor, " .
"expected $expected_type, got $actual_type"
unless $actual_type->isa($expected_type);
}
}
sub CheckedClass::check {
return if $CheckedClass::Skip_Checks;
my ($self) = @_;
my $classname = ref($self);
my $info = $self->merged_type_info();
while (my ($member, $val) = each %$self) {
my $expected_type = $info->{member_types}->{$member};
croak "unknown member $member in $self"
unless $expected_type;
my $actual_type = ref($val);
unless ($actual_type) {
$actual_type = \$val; # e.g., SCALAR(0x123).
$actual_type =~ s/\(.*//; # change "SCALAR(0x123)" to "SCALAR".
}
croak "member $_ type mismatch in $self, " .
"expected $expected_type, got $actual_type"
unless $actual_type->isa($expected_type);
}
}
sub CheckedClass::merged_type_info {
my ($self) = @_;
my $class = ref($self) || $self;
my $cached = $Type_Info_Cache{$class};
return $cached if $cached;
return { } if $class eq 'CheckedClass' or ! $class->isa('CheckedClass');
my (%required, %arg_types, %member_types);
# Get this class's type info.
my $ti = $self->type_info();
while (my ($arg, $type) = each %{$ti->{required_args}}) {
$required{$arg}++;
$arg_types{$arg} = $type;
$member_types{$arg} = $type;
}
while (my ($arg, $type) = each %{$ti->{optional_args}}) {
croak "$class member $arg multiply defined"
if exists $member_types{$arg};
$arg_types{$arg} = $type;
$member_types{$arg} = $type;
}
while (my ($arg, $type) = each %{$ti->{members}}) {
croak "$class member $arg multiply defined"
if exists $member_types{$arg};
$member_types{$arg} = $type;
}
# Merge superclasses' type info.
my @isa = (eval "\@${class}::ISA");
for (@isa) {
my $super_info = $_->merged_type_info();
# required_args is the union of all classes' required args.
for (@{$super_info->{required_args}}) {
$required{$_}++;
}
# arg_types' keys is the union of all classes' arg types.
# The values are the intersection.
while (my ($k, $v) = each %{$super_info->{arg_types}}) {
my $a = \$arg_types{$k};
if (!$$a) {
$$a = $v;
} elsif (!$$a->isa($v)) {
croak "${_} arg $k is incompatible with $_ arg $k"
unless $v->isa($$a);
$$a = $v;
}
}
# member_types is the same as arg_types.
while (my ($k, $v) = each %{$super_info->{member_types}}) {
my $a = \$member_types{$k};
if (!$$a) {
$$a = $v;
} elsif (!$$a->isa($v)) {
croak "${_}->{$k} is incompatible with $_->{$k}"
unless $v->isa($$a);
$$a = $v;
}
}
}
my $list = {
required_args => [ keys %required ],
arg_types => { %arg_types },
member_types => { %member_types },
};
$Type_Info_Cache{$class} = $list;
return $list;
}
# XXX self-test CheckedClass.
1;