Skip to content

Commit cd02b1a

Browse files
committed
Type::Tiny::Enum now has a use_eq optimization
1 parent 77886c7 commit cd02b1a

File tree

2 files changed

+129
-7
lines changed

2 files changed

+129
-7
lines changed

lib/Type/Tiny/Enum.pm

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ sub new_intersection {
109109
sub values { $_[0]{values} }
110110
sub unique_values { $_[0]{unique_values} }
111111
sub constraint { $_[0]{constraint} ||= $_[0]->_build_constraint }
112+
sub use_eq { return $_[0]{use_eq} if exists $_[0]{use_eq}; $_[0]{use_eq} = $_[0]->_build_use_eq }
112113

113114
sub _is_null_constraint { 0 }
114115

@@ -117,6 +118,11 @@ sub _build_display_name {
117118
sprintf( "Enum[%s]", join q[,], @{ $self->unique_values } );
118119
}
119120

121+
sub _build_use_eq {
122+
my $self = shift;
123+
!Type::Tiny::_USE_XS and @{ $self->unique_values } <= 5;
124+
}
125+
120126
sub is_word_safe {
121127
my $self = shift;
122128
return not grep /\W/, @{ $self->unique_values };
@@ -231,17 +237,30 @@ sub can_be_inlined {
231237
sub inline_check {
232238
my $self = shift;
233239

234-
my $xsub;
235240
if ( my $xs_encoding = _xs_encoding( $self->unique_values ) ) {
236-
$xsub = Type::Tiny::XS::get_subname_for( $xs_encoding );
241+
my $xsub = Type::Tiny::XS::get_subname_for( $xs_encoding );
237242
return "$xsub\($_[0]\)" if $xsub && !$Type::Tiny::AvoidCallbacks;
238243
}
239244

240-
my $regexp = $self->_regexp;
241-
my $code =
242-
$_[0] eq '$_'
243-
? "(defined and !ref and m{\\A(?:$regexp)\\z})"
244-
: "(defined($_[0]) and !ref($_[0]) and $_[0] =~ m{\\A(?:$regexp)\\z})";
245+
my $code;
246+
if ( $self->use_eq ) {
247+
use B ();
248+
my %seen;
249+
my @vals = grep { not $seen{$_}++ } @{ $self->values };
250+
if ( @vals == 1 ) {
251+
$code = sprintf( '(defined %s and !ref %s and %s eq %s)', $_[0], $_[0], $_[0], B::perlstring($vals[0]) );
252+
}
253+
else {
254+
$code = sprintf( '(defined %s and !ref %s and (%s))', $_[0], $_[0], join q{ or } => map { sprintf '(%s eq %s)', $_[0], B::perlstring($_) } @vals );
255+
}
256+
}
257+
else {
258+
my $regexp = $self->_regexp;
259+
$code =
260+
$_[0] eq '$_'
261+
? "(defined and !ref and m{\\A(?:$regexp)\\z})"
262+
: "(defined($_[0]) and !ref($_[0]) and $_[0] =~ m{\\A(?:$regexp)\\z})";
263+
}
245264

246265
return "do { $Type::Tiny::SafePackage $code }"
247266
if $Type::Tiny::AvoidCallbacks;
@@ -585,6 +604,46 @@ be passed to the constructor.
585604
If C<< coercion => 1 >> is passed to the constructor, the type will have a
586605
coercion using the C<closest_match> method.
587606
607+
=item C<use_eq>
608+
609+
When generating Perl type checking code, Type::Tiny::Enum will traditionally
610+
test incoming strings for being valid using a single regular expression,
611+
unless L<Type::Tiny::XS> is available and a faster XS check is possible.
612+
613+
From version 2.008006 onwards, if L<Type::Tiny::XS> is unavailable, and the
614+
enum is "small" (five possible values or less), Type::Tiny::Enum will instead
615+
generate code like:
616+
617+
( $_ eq "foo" or $_ eq "bar" or $_ eq "baz" )
618+
619+
... which benchmarks around 5% to 20% faster than C<< /(?:ba[rz]|foo)/ >>.
620+
621+
However, it is possible to manually indicate whether you prefer it to
622+
generate code using C<eq> or regexps by setting C<use_eq> to a boolean
623+
value in the constructor. (If C<use_eq> is not passed to the constructor
624+
at all, Type::Tiny::Enum will try to guess the most efficient technique.)
625+
626+
If you know that certain values in your enumeration are more common than
627+
others, you can "front load" your enumeration with the most common values
628+
so that C<eq> checks those I<first>. This may allow you to speed up certain
629+
checks.
630+
631+
has car_colour => (
632+
is => 'rw',
633+
isa => Type::Tiny::Enum->new( use_eq => 1, values => [qw/
634+
blue
635+
red
636+
grey
637+
white
638+
black
639+
green
640+
yellow
641+
orange
642+
purple
643+
pink
644+
/] );
645+
);
646+
588647
=back
589648
590649
=head2 Methods
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
=pod
2+
3+
=encoding utf-8
4+
5+
=head1 PURPOSE
6+
7+
Checks the C<use_eq> attribute of Type::Tiny::Enum
8+
9+
=head1 AUTHOR
10+
11+
Toby Inkster E<lt>[email protected]E<gt>.
12+
13+
=head1 COPYRIGHT AND LICENCE
14+
15+
This software is copyright (c) 2025 by Toby Inkster.
16+
17+
This is free software; you can redistribute it and/or modify it under
18+
the same terms as the Perl 5 programming language system itself.
19+
20+
=cut
21+
22+
# Force Type::Tiny::XS to not be used
23+
BEGIN {
24+
$ENV{PERL_TYPE_TINY_XS} = 0;
25+
};
26+
27+
use strict;
28+
use warnings;
29+
use lib qw( ./lib ./t/lib ../inc ./inc );
30+
31+
use Test::More;
32+
use Test::Fatal;
33+
use Test::TypeTiny;
34+
use Type::Tiny::Enum;
35+
36+
my $ExplicitUseRE = Type::Tiny::Enum->new( use_eq => 0, values => [qw/ foo bar1 /] );
37+
my $ExplicitUseEq = Type::Tiny::Enum->new( use_eq => 1, values => [qw/ foo bar1 bar2 bar3 bar4 bar5 /] );
38+
my $ImplicitUseRE = Type::Tiny::Enum->new( values => [qw/ foo bar1 bar2 bar3 bar4 bar5 /] );
39+
my $ImplicitUseEq = Type::Tiny::Enum->new( values => [qw/ foo bar1 /] );
40+
41+
ok !$ExplicitUseRE->use_eq, 'accessor for explicit use_eq=>false';
42+
ok $ExplicitUseEq->use_eq, 'accessor for explicit use_eq=>true';
43+
ok !$ImplicitUseRE->use_eq, 'accessor for defaulted use_eq=>false';
44+
ok $ImplicitUseEq->use_eq, 'accessor for defaulted use_eq=>true';
45+
46+
like $ExplicitUseRE->inline_check('$VAR'), qr/\$VAR\s*=~/, 'explicit use_eq=>false seems to generate correct code';
47+
like $ExplicitUseEq->inline_check('$VAR'), qr/\$VAR\s*eq/, 'explicit use_eq=>true seems to generate correct code';
48+
like $ImplicitUseRE->inline_check('$VAR'), qr/\$VAR\s*=~/, 'defaulted use_eq=>false seems to generate correct code';
49+
like $ImplicitUseEq->inline_check('$VAR'), qr/\$VAR\s*eq/, 'defaulted use_eq=>true seems to generate correct code';
50+
51+
should_pass $_, $ExplicitUseRE for 'foo', 'bar1';
52+
should_fail $_, $ExplicitUseRE for 'foo1', 'bar2', undef, [];
53+
54+
should_pass $_, $ExplicitUseEq for 'foo', 'bar1', 'bar2';
55+
should_fail $_, $ExplicitUseEq for 'foo1', undef, [];
56+
57+
should_pass $_, $ImplicitUseEq for 'foo', 'bar1';
58+
should_fail $_, $ImplicitUseEq for 'foo1', 'bar2', undef, [];
59+
60+
should_pass $_, $ImplicitUseRE for 'foo', 'bar1', 'bar2';
61+
should_fail $_, $ImplicitUseRE for 'foo1', undef, [];
62+
63+
done_testing;

0 commit comments

Comments
 (0)