[{"date":"2009-04-28T06:09:31Z","categories":["Perl"],"author":{"name":"Florian Ragwitz","email":"rafl@fsfe.org","keyid":"742f2a428e635a5e"},"tags":[{"perl":0},{"types":0},{"moose":0},{"syntax":0}],"modified":"2009-04-28T06:20:16Z","uri":"http://perldition.org/articles/Implementing%20Typed%20Lexical%20Variables.pod","signed":1,"summary":" For quite some time perl provided a form of …","xhtml":"
For quite some time perl provided a form of my declarations that\nincludes a type name, like this:
my Str $x = 'foo';\n\n
However, that didn't do anything useful, until Vincent Pit came along\nand wrote the excellent\nLexical::Types\nmodule, which allows you to extend the semantics of typed lexicals and\nactually make them do something useful. For that, it simply invokes a\ncallback for every my declaration with a type in the scopes it is\nloaded. Within that callback you get the variable that is being\ndeclared as well as the name of the type used in the declaration.
We also have Moose type constraints and the great\nMooseX::Types module,\nthat allows us to define our own type libraries and import the type\nconstraints into other modules.
\nLet's glue those modules together. Consider this code:
\nuse MooseX::Types::Moose qw/Int/;\nuse Lexical::Types;\nmy Int $x = 42;\n\n
The first problem is that the perl compiler expects a package with the\nname of the type used in my to exist. If there's no such package\ncompilation will fail.
Creating top-level namespaces for all the types we want to use would\nobviously suck. Luckily the compiler will also try to look for a\nfunction with the name of the type in the current scope. If that\nexists and is inlineable, it will call that function and use the\nreturn value as a package name.
\nIn the above code snippet an Int function already exists. We\nimported that from MooseX::Types::Moose. Unfortunately it isn't\ninlineable. Even if it were, compilation would still fail, because it\nwould return a Moose::Meta::TypeConstraint instead of a valid\npackage name.
To fix that, let's rewrite the code to this:
\nuse MooseX::Types::Moose qw/Int/;\nuse MooseX::Lexical::Types qw/Int/;\nmy Int $x = 42;\n\n
Let's also write a MooseX::Lexical::Types module that replaces\nexisting imported type exports with something that can be inlined and\nreturns an existing package name based on the type constraint's name.
\npackage MooseX::Lexical::Types;\n\nuse Class::MOP;\nuse MooseX::Types::Util qw/has_available_type_export/;\nuse namespace::autoclean;\n\nsub import {\n my ($class, @args) = @_;\n my $caller = caller();\n\n my $meta = Class::MOP::class_of($caller) || Class::MOP::Class->initialize($caller);\n\n for my $type_name (@args) {\n # get the type constraint by introspecting the caller\n my $type_constraint = has_available_type_export($caller, $type_name);\n\n my $package = 'MooseX::Lexical::Types::TYPE::' . $type_constraint->name;\n Class::MOP::Class->create($package);\n $meta->add_package_symbol('&'.$type_name => sub () { $package });\n }\n\n Lexical::Types->import; # enable Lexical::Types for the caller\n}\n\n1;\n\n
With that the example code now compiles. Unfortunately it breaks every\nother usecase of MooseX::Types. The export will still need to return a\nMoose::Meta::TypeConstraint at run time so this will continue to\nwork:
has some_attribute => (is => 'ro', isa => Int);\n\n
So instead of returning a plain package name from our exported\nfunction we will return an object that delegates all method calls to\nthe actual type constraint, but evaluates to our special package name\nwhen used as a string:
\nmy $decorator = MooseX::Lexical::Types::TypeDecorator->new($type_constraint);\n$meta->add_package_symbol('&'.$type_name => sub () { $decorator });\n\n
and:
\npackage MooseX::Lexical::Types::TypeDecorator;\nuse Moose;\nuse namespace::autoclean;\n\n# MooseX::Types happens to already have a class that doesn't do much\n# more than delegating to a real type constraint!\nextends 'MooseX::Types::TypeDecorator';\n\nuse overload '""' => sub {\n 'MooseX::Lexical::Types::TYPE::' . $_[0]->__type_constraint->name\n};\n\n1;\n\n
Now we're able to use Int as usual and have Lexical::Types invoke\nits callback on MooseX::Lexical::Types::TYPE::Int. Within that\ncallback we will need the real type constraint again, but as it is\ninvoked as a class method with no good way to pass in additional\narguments, we will need to store the type constraint somewhere. I\nchoose to simply add a method to the type class we create when\nconstructing our export. After that, all we need is to implement our\nLexical::Types callback. We will put that in a class all our type\nclasses will inherit from:
Class::MOP::Class->create(\n $package => (\n superclasses => ['MooseX::Lexical::Types::TypedScalar'],\n methods => {\n get_type_constraint => sub { $type_constraint },\n },\n ),\n);\n\n
The Lexical::Types callback will now need to tie things together by\nmodifying the declared variable so it will automatically validate\nvalues against the type constraint when being assigned to. There are\nseveral ways of doing this. Using tie on the declared variable\nwould probable be the easiest thing to do. However, I decided to use\nVariable::Magic\n(also written by Vincent Pit - did I mention he's awesome?), because\nit's mostly invisible at the perl level and also performs rather well\n(not that it'd matter, given that validation itself is relatively\nslow):
package MooseX::Lexical::Types::TypedScalar;\n\nuse Carp qw/confess/;\nuse Variable::Magic qw/wizard cast/;\nuse namespace::autoclean;\n\nmy $wiz = wizard\n # store the type constraint in the data attached to the magic\n data => sub { $_[1]->get_type_constraint },\n # when assigning to the variable, fail if we can't validate the\n # new value ($_[0]) against the type constraint ($_[1])\n set => sub {\n if (defined (my $msg = $_[1]->validate(${ $_[0] }))) {\n confess $msg;\n }\n ();\n };\n\nsub TYPEDSCALAR {\n # cast $wiz on the variable in $_[1]. pass the type package name\n # in $_[0] to the wizard's data construction callback.\n cast $_[1], $wiz, $_[0];\n ();\n}\n\n1;\n\n
With this, our example code now works. If someone wants to assign,\nsay, 'foo' to the variable declared as my Int $x our magic\ncallback will be invoked, try to validate the value against the type\nconstraint and fail loudly. WIN!
The code for all this is available\ngithub and should also\nbe on CPAN shortly.
\nYou might notice warnings about mismatching prototypes. Those are\ncaused by Class::MOP and fixed in the git version of it, so they'll go\naway with the next release.
\nThere's still a couple of caveats, but please see the documentation\nfor that.
\n\n\n