The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

typesafety.pm - compile-time type usage static analysis

SYNOPSIS

  package main;
  use typesafety;

  # use typesafety 'summary';

  my FooBar $foo :typed; # alternate syntax:    declare FooBar => my $a;
  my FooBar $bar :typed; # establish type-checked variables
  my BazQux $baz :typed; # my <type/package> <variable> :typed;

  $foo = new FooBar;     # this is okey, because $foo holds FooBars
  $bar = $foo;           # this is okey, because $bar also holds FooBars
  # $foo = 10;           # this would throw an error - 10 is not a FooBar
  # $baz = $foo;         # not allowed - FooBar isn't a BazQux
  $foo = $baz;           # is allowed -  BazQux is a FooBar because of inheritance
  $bar = $foo->foo();    # this is okey, as FooBar::foo() returns FooBars also

  typesafety::check();   # perform type check static analysis

  #

  package FooBar;
  use typesafety;

  # unneeded - new() defaults to prototype to return same type as package
  # proto 'new', returns => 'FooBar'; 

  proto 'foo', returns => 'FooBar'; 
  # proto 'methodname', returns => 'FooBar', takes => 'Type', 'Type', 'Type';


  sub new {
    my $type = shift; $type = ref $type if ref $type;
    bless [], $type;
  }

  sub foo { my $me = shift; return $me->new(); } 

  #

  package BazQux;
  use typesafety;
  @ISA = 'FooBar';

DESCRIPTION

Prevents you from mistakenly bathing cats.

Scenarios:

Somewhere between point A and point B, a value stops being what you expect. You start inserting locks of checks to figure out where the value went wrong.
Sometimes a return value is what you want, but sometimes other things come back, so you have to continiously add checks in the code.
All objects are basically the same. Things that want an object will take any object and try to deal with it, often by just throwing errors if a given method doesn't exist (->can() returns false). This leads to large numbers of checks, yet no number of checks make you feel confident that you've covered everything that can go wrong.
You're limited in how large of programs you can write, because you can't keep everything straight after a while. And you're tired of writing checks.

Failure to keep track what kind of data is in a given variable or returned from a given method is an epic source of confusion and frustration during debugging.

Given a ->get_pet() method, you might try to bathe the output. If it always a dog during testing, everything is fine, but sooner or later, you're going to get a cat, and that can be rather bloody.

Welcome to Type Safety. Type Safety means knowing what kind of data you have (atleast in general - it may be a subclass of the type you know you have). Because you always know what kind of data it is, you see in advance when you try to use something too generic (like a pet) where you want something more specific (like a dog, or atleast a pet that implements the "washable" interface).

Think of Type Safety as a new kind of variable scoping - instead of scoping where the variables can be seen from, you're scoping what kind of data they might contain.

"Before hand" means when the program is parsed, not while it is running. This prevents bugs from "hiding". I'm sure you're familiar with evil bugs, lurking in the dark, il-used corners of the program, like so many a grue. Like Perl's use strict and use warnings and use diagnostics, potential problems are brought to your attention before they are proven to be a problem by tripping on them while the program happens on that nasty part of code. You might get too much information, but you'll never have to test every aspect of the program to try to uncover these sorts of warnings. Now you understand the difference between "run time diagnostics" and "compile time warnings".

Asserts in the code, checking return values manually, are an example of run-time type checking:

  # we die unexpectedly, but atleast bad values don't creep around!
  # too bad our program is so ugly, full of checks and possible bad
  # cases to check for...

  my $foo = PetStore->get_pet();
  $foo->isa('Dog') or die; 

Run-time type checking misses errors unless a certain path of execution is taken, leaving little time bombs to go off, showing up later. More importantly, it clutters up the code with endless "debugging" checks, known as "asserts", from the C language macro of the same name.

Type Safety is a cornerstone of Object Oriented programming. It works with Polymorphism and Inheritance (including Interface Inheritance).

Use typesafety.pm while developing. Comment out the typesafety::check() statement when placing the code into production. This emulates what is done with compiled languages - types are checked only after the last time changes are made to the code. The type checking is costly in terms of CPU, and as long as the code stays the same, the results won't change. If everything was type safe the last time you tested, and you haven't changed anything, then it still is.

A very few, very specific things are inspected in the program when typesafety::check() is called:

  $a = $b;

Variable assignment. Rules are only applied to variables that are "type safe" - a type safe variable was declared using one of the two constructs shown in the SYNOPSIS. If it isn't type safe, none of these rules apply. Otherwise, $b must be the same type as $a, or a subclass of $a's type. In other words, the types must "match".

  $a->meth();

Method call. If $a is type safe, then the method meth() must exist in whichever package $a was prototyped to hold a reference to. Note that type safety can't keep you from trying to use a null reference (uninitialized variable), only from trying to call methods that haven't been proven to be part of the module they're prototyped to hold a reference to. If the method hasn't been prototyped in that module, then a ->can() test is done at compile time. Inheritance is handled this way.

  $a = new Foo;

Package constructors are always assumed to return an object of the same type as their package. In this case, $a->isa('Foo') is expected to be true after this assignment. This may be overridden with a prototype for your abstract factory constructors (which really belong in another method anyway, but I'm feeling generous). The return type of Foo->new() must match the type of $a, as a seperate matter. To match, it must match exactly or else be a subclass of $a expects. This is just like the simple case of "variable assignment", above. If new() has arguments prototyped for it, the arguments types must also match. This is just like "Method call", above.

  $a = $foo->new();

Same as above. If $foo is type checked and $a is not, then arguments to the new() method are still checked against any prototype. If $a is type checked, then the return value of new() must match. If no prototype exists for new() in whatever package $foo belongs to, then, as above, the return value is assumed to be the same as the package $foo belongs to. In other words, in normal circumstances, you don't have to prototype methods.

  $b = $a->bar();

As above: the return type of bar() must be the same as, or a subclass of, $b's, if $b is type safe. If $a is type safe and there is a prototype on bar(), then argument types are inforced.

  $b = $a->bar($a->baz(), $z->qux());

The above rules apply recursively: if a method call is made to compute an argument, and the arguments of the bar() method are prototyped, then the return values of method calls made to compute the arguments must match the prototype. Any of the arguments in the prototype may be undef, in which case no particular type is enforced. Only object types are enforced - if you want to pass an array reference, then bless that array reference into a package and make it an object.

  typesafety::check(); 

This must be done after setting things up to perform actual type checking, or it can be commented out for production. The module will still need to be used to provide the proto(), and add the attribute.pm interface handlers.

Giving the 'summary' argument to the use typesafety line generates a report of defined types when typesafety::check() is run:

  typesafety.pm status report:
  ----------------------------
  variable $baz, type BazQux, defined in package main, file test.7.pl, line 36
  variable $bar, type FooBar, defined in package main, file test.7.pl, line 34
  variable $foo, type FooBar, defined in package main, file test.7.pl, line 33

I don't know what this is good for except warm fuzzy feelings.

You can also specify a 'debug' flag, but I don't expect it will be very helpful to you.

OTHER MODULES

Class::Contract by Damian Conway. Let me know if you notice any others. Class::Contract only examines type safety on arguments to and from method calls. It doesn't delve into the inner workings of a method to make sure that types are handled correctly in there. This module covers the same turf, but with less syntax and less bells and whistles. This module is more natural to use, in my opinion.

To the best of my knowledge, no other module attempts to do what this modules, er, attempts to do.

Object::PerlDesignPatterns by myself. Documentation. Deals with many concepts surrounding Object Oriented theory, good design, and hotrodding Perl. The current working version is always at http://perldesignpatterns.com.

DIAGNOSTICS

  unsafe assignment:  in package main, file test.7.pl, line 42 - variable $baz, 
  type BazQux, defined in package main, file test.7.pl, line 36 cannot hold method 
  foo, type FooBar, defined in package FooBar, file test.7.pl, line 6 at 
  typesafety.pm line 303.

There are actually a lot of different diagnostic messages, but they are all somewhat similar. Either something was being assigned to something it shouldn't have been, or else something is being passed in place of something it shouldn't be. The location of the relavent definitions as well the actual error are included, along with the line in typesafety.pm, which is only useful to me.

BUGS

My favorite section!

Yes, every module I write mentions Damian Conway =)

This module uses the experimental attributes.pm module. As such, this module is also experimental (as if it doesn't have enough problems already). It may break in future versions of Perl. Also, we're intimately tied to the bytecode tree, the structure of which could easily change in future versions of Perl. This works on my 5.9.0 pre-alpha. It might not work at all on what you have.

Only operations on lexical my variables are supported. Attempting to assign a global to a typed variable will be ignored - type errors won't be reported. Global variables themselves cannot be type checked. Again, all doable, just ran out of steam.

Only operations on methods using the $ob->method(args) syntax is supported - function calls are not prototyped nor recognized. Stick to method calls for now.

Types should be considered to match if the last part matches - Foo::Bar->isa('Bar') would be true. This might take some doing. Workaround to :: not being allowed in attribute-prototypes. Presently, programs with nested classes, like Foo::Bar, cannot have these types assigned to variables. No longer true - the declare() syntax is a work-around to this.

Can't solve complex expressions in arguments to methods or on the right of assignments. Anything more complex than a method call stumps the thing. In theory, every construct could be supported, but that would take a lot of code to do. Java essentially does this - the compiler knows how to grok the argument and return types of every operator, expression, construct, library method, and user code method. We have to simply ignore things we don't understand.

We use B::Generate just for the ->sv() method. Nothing else. I promise! We're not modifying the byte code tree, just reporting on it. I do have some ideas for using B::Generate, but don't go off thinking that this module does radical run time self modifying code stuff.

Todo: Integration with interface.pm (don't allow method calls unless that sub is there) and with Class::Lexical (typed lexical-based classes).

The root (code not in functions) of main:: is checked, but not the roots of other modules. I don't know how to get a handle on them. Sorry. Methods and functions in main:: and other namespaces that use typesafety; get checked, of course.

Having to call a "check" function is kind of a kludge but I haven't thought of a better way. Modules we use have a chance to run at the root level, which lets the proto() functions all run, if we are used after they are, but the main package has no such benefit. Running at CHECK time doesn't let anything run.

The B tree matching, navigation, and type solving logic should be presented as a reusable API, and a module specific to this task should use that module. After I learn what the pattern is and fascilities are really needed, I'll consider this.

Argh! Perl fails to link to B::Generate's .so when doing a "make test", so testing is impossible. Sorry. You get one test, and it is a null test.

Some things just plain might not work as described. Let me know.

SEE ALSO

http://perldesignpatterns.com/?TypeSafety - look for updated documentation on this module here - this doc is kind of sparse.

http://www.c2.com/cgi/wiki?NoseJobRefactoring

Class::Contract, by Damian Conway

Object::PerlDesignPatterns, by myself.

The test.pl file that comes with this distribution demonstrates exhaustively everything that is allowed and everything that is not.

AUTHOR

Scott Walters - scott@slowass.net

COPYRIGHT

Distribute under the same terms as Perl itself.