DEV Community

Al Newkirk
Al Newkirk

Posted on

My Perl Weekly Challenge

Really, how hard could it be?

All this talk about types, objects, and systems, got me to thinking, "what would it take to create a 100% backwards-compatible pure Perl proof-of-concept for optionally typable subroutine signatures". I mean really, how hard could it be? So I started sketching out some ideas and here's what I came up with:

use typing;

sub greet :Function(string, number) :Return() {
  my ($name, $count) = &_;

  print "Hi $name, you have $count messages waiting ...\n";
}
Enter fullscreen mode Exit fullscreen mode

This final sketch felt very Perlish and I had an inkling that I might be able to tie all the concepts being used together, and I did. It actually works, and it's fast. Let's break down what's actually happening here.

use typing;
Enter fullscreen mode Exit fullscreen mode

I don't particularly like or care about the name, I had to call it something, the code is about resolving types, so I called it "typing". When you import the package the framework installs two magic symbols. Oh, yes, btw, once I made it all work, I decided to extend it so that any type or object system could hook into it to allow the resolution of their own types using this system, so yes, it's a framework.

sub greet :Function(string, number) :Return();
Enter fullscreen mode Exit fullscreen mode

The "greet" subroutine is just a plain ole subroutine. No source filter, no Perl keyword API, no XS, no high-magic. Here we're using old school "attributes" to annotate the subroutine and denote whether it's a function or method, and whether it has a return value or not.

sub greet :Method(object, string) :Return(object);
Enter fullscreen mode Exit fullscreen mode

Declaring a subroutine as a method doesn't do anything special. No automagic unpacking, no implied/inferred first argument. The same is true for the "return" declaration. In fact, the annotations aren't involved in the execution of your program unless to you use the magic "unpacker" function.

# use this ...
my ($name, $count) = &_;

# instead of this ...
my ($name, $count) = @_;
Enter fullscreen mode Exit fullscreen mode

This works due to a little-known Perl trick that only the most neck-beardiest of Perl hackers understand, and me, which is what happens when you call a subroutine using the ampersand without any arguments, i.e. you can operate on the caller's argument list. By naming the function _, and requiring the use of the ampersand, we've created a cute little synonym for the @_ variable.

sub greet :Function(string, number) :Return() {
  my ($name, $count) = &_;
  # ...
}
Enter fullscreen mode Exit fullscreen mode

Here's what's happening. The "unpacker" function gets the typed argument list for the calling subroutine, i.e. it gets its signature, then it iterates over the arguments calling the configured validator for each type expression specified at each position in the argument list.

greet() # error
greet({}) # error
greet('bob') # error
greet('bob', 2) # sweet :)
Enter fullscreen mode Exit fullscreen mode

But what happens if you provide more arguments than the signature has type expressions for? The system is designed to use the last type specified to validate all out-of-bounds arguments, which means greet('bob', 2..10) works and passes the type checks, but greet('bob', 2, 'bob', 2) doesn't because the second 'bob' isn't a number. Make sense? Right, but what about the framework bit? First, let's see what it looks like to hook into the framework as a one-off:

use typing;

typing::set_spaces('main', 'main');

sub main::string::make {
  # must return (value, valid?, explanation)
  ($_[0], 1, '')
}

sub greet :Function(string) {
  my ($name) = &_;
  print "Hi $name\n";
}
Enter fullscreen mode Exit fullscreen mode

This example is one of the simplest hooks. The set_spaces function says, the caller is the "main" package, and we should look for types under the "main" namespace by prefixing the type name with "main" and looking for a subroutine called "make". The "make" subroutine must return a list in the form of (value, valid?, explanation). This approach expects each type to correspond to a package (with a "make" routine), but maybe you like the type library/registry approach. Another way to hook into the system is to bring your own resolver:

use typing;

typing::set_resolver('main', ['main', 'resolver']);

sub resolver {
  # accepts (package, type-expr, value)
  # returns (value, valid?, explanation)
  # maybe you'll do something like $registry->get(type-expr)->validate(value)
  ($_[0], 1, '')
}

sub greet :Function(string) {
  my ($name) = &_;
  print "Hi $name\n";
}
Enter fullscreen mode Exit fullscreen mode

This example uses the set_resolver function and says, the caller is the "main" package, and we should resolve all types in the "main" package using the main::resolver subroutine. The resolver must return a list in the form of (value, valid?, explanation). But wait, there's more, ... we can use the framework's API to get the subroutine metadata to further automate our programs, for example:

use typing;

typing::retrieve('main', 'method', 'greet')
# [
#   'MyApp::Object',
#   'string',
#   'number'
# ]

typing::retrieve('main', 'return', 'greet')
# [
#   'MyApp::Object'
# ]

sub greet :Function(MyApp::Object, string, number) :Return(MyApp::Object) {
  # ...
}
Enter fullscreen mode Exit fullscreen mode

I'm supposed to be working on a completely different project right now, but this idea captivated me and so I lost a couple of days of productivity. Maybe others will find this idea interesting. If you're interested in the source code for this concept you can find it here!

Sources

Type libraries

MooseX::Types

Type::Tiny

Specio

Subroutine signatures

Function::Parameters

Method::Signatures

registry/routines

End Quote

"Software is like art and the only justification you need for creating it is, 'because I felt like it'" - Andy Wardley

Authors

Awncorp, awncorp@cpan.org

Top comments (0)