Front page | perl.perl5.porters |
Postings from November 2019
Re: Type and Value Constraints and Coercions
From:
Dave Mitchell
Date:
November 29, 2019 14:44
Subject:
Re: Type and Value Constraints and Coercions
Message ID:
20191129144438.GQ3620@iabyn.com
[I'm Forwarding this on behalf of Toby Inkster]
This is a reply to Dave Mitchell's email, but it will probably get
threaded badly as I've only just subscribed to perl5-porters and don't
have the original email to click "reply" on.
This is all pretty over-engineered. It can be simplified to something
that would work well with existing type implementations such as
MooseX::Types, MouseX::Types, Type::Tiny, and Specio.
First, make this:
sub f ($x is Int) {
...;
}
effectively a shorthand for:
sub f {
my ($x) = @_;
Int->check($x) or Carp::croak(Int->get_message($x));
}
Second, define these:
use Scalar::Util;
sub UNIVERSAL::check {
my ($class, $object) = @_;
blessed($object) && !blessed($class) && $object->DOES($class);
}
sub UNIVERSAL::get_message {
my ($class, $value) = @_;
"$value is not $class";
}
Third, nothing. There is no third.
Once you've got those two things working, then the following will
"just work":
use MooseX::Types::Common qw(PositiveInt);
sub add_counts ($x is PositiveInt, $y is PositiveInt) {
return $x + $y;
}
This works because PositiveInt is just a sub that returns an
object with `check` and `get_message` methods.
And this will also "just work" (thanks to UNIVERSAL):
sub fetch_page ($ua is HTTP::Tiny, $url) {
my $response = $ua->get($url);
$response->{success} or die $response->{message};
return $response->{content};
}
Yeah, `check` and `get_message` are pretty generic-sounding methods
to be adding to UNIVERSAL and there are arguments in favour of, say,
`CHECK` and `GET_MESSAGE`.
The advantage of `check` and `get_message` is that the following
already works out of the box in MooseX::Types, MouseX::Types, and
Type::Tiny, and can be made to work with Specio with a pretty small
shim.
Int->check($x) or Carp::croak(Int->get_message($x));
Shim for Specio is:
sub Int () { return t("Int") }
> Also, the nested HashRef[ArrayRef[Int]] form quickly becomes a
> performance nightmare, with every element of the AoH having to
> be checked for Int-ness on every call to the function.
It's not as bad for performance as you might think.
MouseX::Types and Type::Tiny are capable of checking
HashRef[ArrayRef[Int]] with a single XS sub call. MooseX::Types
and Specio will check it without XS, but it's still a single
sub call, just with a lot of loops and regexp checks.
That said, there are performance improvements that can be made.
One would be at compile time, when Perl sees:
sub f ($x is Int) {
...;
}
It would call:
$code = eval { Int->inline_check('$x') };
The Int object would return a string of Perl code like:
q{ defined($x) && !ref($x) && $x =~ /^-?[0-9]+$/ }
And this would be inlined into the function like:
sub f {
my ($x) = @_;
do {
defined($x) && !ref($x) && $x =~ /^-?[0-9]+$/
} or Carp::croak(Int->get_message($x));
}
(Note I'm using the block form of eval when Perl fetches the inline
code, so Perl isn't evaluating the string of code at run time. It
just allows Int to throw an exception if it's unable to inline the
check. Some checks are hard or impossible to inline.)
Once again, there's discussion to be had about the name of the
method `inline_check`, but Type::Tiny and Specio already offer an
`inline_check` method exactly like this. And Moose offers
`_inline_check`. Mouse offers neither, so Perl would just fall
back to doing Int->check($x) to check the value.
Coercions are a whole different kettle of fish and how they interact
with aliasing and read only parameters can get confusing. For this
reason, I'd recommend simply leaving them out of signatures, at least
while people get used to having type constraints in signatures.
People can coerce stuff manually in the body of their sub. This is not
hard, it's probably what they're doing already, and it's almost
certainly more readable than any syntax you can squeeze into the
signature.