504 lines
15 KiB
Perl
504 lines
15 KiB
Perl
package Specio;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use 5.008;
|
|
|
|
our $VERSION = '0.46';
|
|
|
|
1;
|
|
|
|
# ABSTRACT: Type constraints and coercions for Perl
|
|
|
|
__END__
|
|
|
|
=pod
|
|
|
|
=encoding UTF-8
|
|
|
|
=head1 NAME
|
|
|
|
Specio - Type constraints and coercions for Perl
|
|
|
|
=head1 VERSION
|
|
|
|
version 0.46
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
package MyApp::Type::Library;
|
|
|
|
use Specio::Declare;
|
|
use Specio::Library::Builtins;
|
|
|
|
declare(
|
|
'PositiveInt',
|
|
parent => t('Int'),
|
|
inline => sub {
|
|
$_[0]->parent->inline_check( $_[1] )
|
|
. ' && ( '
|
|
. $_[1]
|
|
. ' > 0 )';
|
|
},
|
|
);
|
|
|
|
# or ...
|
|
|
|
declare(
|
|
'PositiveInt',
|
|
parent => t('Int'),
|
|
where => sub { $_[0] > 0 },
|
|
);
|
|
|
|
declare(
|
|
'ArrayRefOfPositiveInt',
|
|
parent => t(
|
|
'ArrayRef',
|
|
of => t('PositiveInt'),
|
|
),
|
|
);
|
|
|
|
coerce(
|
|
'ArrayRefOfPositiveInt',
|
|
from => t('PositiveInt'),
|
|
using => sub { [ $_[0] ] },
|
|
);
|
|
|
|
any_can_type(
|
|
'Duck',
|
|
methods => [ 'duck_walk', 'quack' ],
|
|
);
|
|
|
|
object_isa_type('MyApp::Person');
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
The C<Specio> distribution provides classes for representing type constraints
|
|
and coercion, along with syntax sugar for declaring them.
|
|
|
|
Note that this is not a proper type system for Perl. Nothing in this
|
|
distribution will magically make the Perl interpreter start checking a value's
|
|
type on assignment to a variable. In fact, there's no built-in way to apply a
|
|
type to a variable at all.
|
|
|
|
Instead, you can explicitly check a value against a type, and optionally
|
|
coerce values to that type.
|
|
|
|
My long-term goal is to replace Moose's built-in types and L<MooseX::Types>
|
|
with this module.
|
|
|
|
=head1 WHAT IS A TYPE?
|
|
|
|
At it's core, a type is simply a constraint. A constraint is code that checks
|
|
a value and returns true or false. Most constraints are represented by
|
|
L<Specio::Constraint::Simple> objects. However, there are other type
|
|
constraint classes for specialized kinds of constraints.
|
|
|
|
Types can be named or anonymous, and each type can have a parent type. A
|
|
type's constraint is optional because sometimes you may want to create a named
|
|
subtype of some existing type without adding additional constraints.
|
|
|
|
Constraints can be expressed either in terms of a simple subroutine reference
|
|
or in terms of an inline generator subroutine reference. The former is easier
|
|
to write but the latter is preferred because it allow for better optimization.
|
|
|
|
A type can also have an optional message generator subroutine reference. You
|
|
can use this to provide a more intelligent error message when a value does not
|
|
pass the constraint, though the default message should suffice for most cases.
|
|
|
|
Finally, you can associate a set of coercions with a type. A coercion is a
|
|
subroutine reference (or inline generator, like constraints), that takes a
|
|
value of one type and turns it into a value that matches the type the coercion
|
|
belongs to.
|
|
|
|
=head1 BUILTIN TYPES
|
|
|
|
This distribution ships with a set of builtin types representing the types
|
|
provided by the Perl interpreter itself. They are arranged in a hierarchy as
|
|
follows:
|
|
|
|
Item
|
|
Bool
|
|
Maybe (of `a)
|
|
Undef
|
|
Defined
|
|
Value
|
|
Str
|
|
Num
|
|
Int
|
|
ClassName
|
|
Ref
|
|
ScalarRef (of `a)
|
|
ArrayRef (of `a)
|
|
HashRef (of `a)
|
|
CodeRef
|
|
RegexpRef
|
|
GlobRef
|
|
FileHandle
|
|
Object
|
|
|
|
The C<Item> type accepts anything and everything.
|
|
|
|
The C<Bool> type only accepts C<undef>, C<0>, or C<1>.
|
|
|
|
The C<Undef> type only accepts C<undef>.
|
|
|
|
The C<Defined> type accepts anything I<except> C<undef>.
|
|
|
|
The C<Num> and C<Int> types are stricter about numbers than Perl
|
|
is. Specifically, they do not allow any sort of space in the number, nor do
|
|
they accept "Nan", "Inf", or "Infinity".
|
|
|
|
The C<ClassName> type constraint checks that the name is valid I<and> that the
|
|
class is loaded.
|
|
|
|
The C<FileHandle> type accepts either a glob, a scalar filehandle, or anything
|
|
that isa L<IO::Handle>.
|
|
|
|
All types accept overloaded objects that support the required operation. See
|
|
below for details.
|
|
|
|
=head2 Overloading
|
|
|
|
Perl's overloading is horribly broken and doesn't make much sense at all.
|
|
|
|
However, unlike Moose, all type constraints allow overloaded objects where
|
|
they make sense.
|
|
|
|
For types where overloading makes sense, we explicitly check that the object
|
|
provides the type overloading we expect. We I<do not> simply try to use the
|
|
object as the type in question and hope it works. This means that these checks
|
|
effectively ignore the C<fallback> setting for the overloaded object. In other
|
|
words, an object that overloads stringification will not pass the C<Bool> type
|
|
check unless it I<also> overloads boolification.
|
|
|
|
Most types do not check that the overloaded method actually returns something
|
|
that matches the constraint. This may change in the future.
|
|
|
|
The C<Bool> type accepts an object that implements C<bool> overloading.
|
|
|
|
The C<Str> type accepts an object that implements string (C<q{""}>)
|
|
overloading.
|
|
|
|
The C<Num> type accepts an object that implements numeric (C<'0+'}>)
|
|
overloading. The C<Int> type does as well, but it will check that the
|
|
overloading returns an actual integer.
|
|
|
|
The C<ClassName> type will accept an object with string overloading that
|
|
returns a class name.
|
|
|
|
To make this all more confusing, the C<Value> type will I<never> accept an
|
|
object, even though some of its subtypes will.
|
|
|
|
The various reference types all accept objects which provide the appropriate
|
|
overloading. The C<FileHandle> type accepts an object which overloads
|
|
globification as long as the returned glob is an open filehandle.
|
|
|
|
=head1 PARAMETERIZABLE TYPES
|
|
|
|
Any type followed by a type parameter C<of `a> in the hierarchy above can be
|
|
parameterized. The parameter is itself a type, so you can say you want an
|
|
"ArrayRef of Int", or even an "ArrayRef of HashRef of ScalarRef of ClassName".
|
|
|
|
When they are parameterized, the C<ScalarRef> and C<ArrayRef> types check that
|
|
the value(s) they refer to match the type parameter. For the C<HashRef> type,
|
|
the parameter applies to the values (keys are never checked).
|
|
|
|
=head2 Maybe
|
|
|
|
The C<Maybe> type is a special parameterized type. It allows for either
|
|
C<undef> or a value. All by itself, it is meaningless, since it is equivalent
|
|
to "Maybe of Item", which is equivalent to Item. When parameterized, it
|
|
accepts either an C<undef> or the type of its parameter.
|
|
|
|
This is useful for optional attributes or parameters. However, you're probably
|
|
better off making your code simply not pass the parameter at all This usually
|
|
makes for a simpler API.
|
|
|
|
=head1 REGISTRIES AND IMPORTING
|
|
|
|
Types are local to each package where they are used. When you "import" types
|
|
from some other library, you are actually making a copy of that type.
|
|
|
|
This means that a type named "Foo" in one package may not be the same as "Foo"
|
|
in another package. This has potential for confusion, but it also avoids the
|
|
magic action at a distance pollution that comes with a global type naming
|
|
system.
|
|
|
|
The registry is managed internally by the Specio distribution's modules, and is
|
|
not exposed to your code. To access a type, you always call C<t('TypeName')>.
|
|
|
|
This returns the named type or dies if no such type exists.
|
|
|
|
Because types are always copied on import, it's safe to create coercions on
|
|
any type. Your coercion from C<Str> to C<Int> will not be seen by any other
|
|
package, unless that package explicitly imports your C<Int> type.
|
|
|
|
When you import types, you import every type defined in the package you import
|
|
from. However, you I<can> overwrite an imported type with your own type
|
|
definition. You I<cannot> define the same type twice internally.
|
|
|
|
=head1 CREATING A TYPE LIBRARY
|
|
|
|
By default, all types created inside a package are invisible to other
|
|
packages. If you want to create a type library, you need to inherit from
|
|
L<Specio::Exporter> package:
|
|
|
|
package MyApp::Type::Library;
|
|
|
|
use parent 'Specio::Exporter';
|
|
|
|
use Specio::Declare;
|
|
use Specio::Library::Builtins;
|
|
|
|
declare(
|
|
'Foo',
|
|
parent => t('Str'),
|
|
where => sub { $_[0] =~ /foo/i },
|
|
);
|
|
|
|
Now the MyApp::Type::Library package will export a single type named
|
|
C<Foo>. It I<does not> re-export the types provided by
|
|
L<Specio::Library::Builtins>.
|
|
|
|
If you want to make your library re-export some other libraries types, you can
|
|
ask for this explicitly:
|
|
|
|
package MyApp::Type::Library;
|
|
|
|
use parent 'Specio::Exporter';
|
|
|
|
use Specio::Declare;
|
|
use Specio::Library::Builtins -reexport;
|
|
|
|
declare( 'Foo, ... );
|
|
|
|
Now MyApp::Types::Library exports any types it defines, as well as all the
|
|
types defined in L<Specio::Library::Builtins>.
|
|
|
|
=head1 DECLARING TYPES
|
|
|
|
Use the L<Specio::Declare> module to declare types. It exports a set of helpers
|
|
for declaring types. See that module's documentation for more details on these
|
|
helpers.
|
|
|
|
=head1 USING SPECIO WITH L<Moose>
|
|
|
|
This should just work. Use a Specio type anywhere you'd specify a type.
|
|
|
|
=head1 USING SPECIO WITH L<Moo>
|
|
|
|
Using Specio with Moo is easy. You can pass Specio constraint objects as
|
|
C<isa> parameters for attributes. For coercions, simply call C<<
|
|
$type->coercion_sub >>.
|
|
|
|
package Foo;
|
|
|
|
use Specio::Declare;
|
|
use Specio::Library::Builtins;
|
|
use Moo;
|
|
|
|
my $str_type = t('Str');
|
|
has string => (
|
|
is => 'ro',
|
|
isa => $str_type,
|
|
);
|
|
|
|
my $ucstr = declare(
|
|
'UCStr',
|
|
parent => t('Str'),
|
|
where => sub { $_[0] =~ /^[A-Z]+$/ },
|
|
);
|
|
|
|
coerce(
|
|
$ucstr,
|
|
from => t('Str'),
|
|
using => sub { return uc $_[0] },
|
|
);
|
|
|
|
has ucstr => (
|
|
is => 'ro',
|
|
isa => $ucstr,
|
|
coerce => $ucstr->coercion_sub,
|
|
);
|
|
|
|
The subs returned by Specio use L<Sub::Quote> internally and are suitable for
|
|
inlining.
|
|
|
|
=head1 USING SPECIO WITH OTHER THINGS
|
|
|
|
See L<Specio::Constraint::Simple> for the API that all constraint objects
|
|
share.
|
|
|
|
=head1 L<Moose>, L<MooseX::Types>, and Specio
|
|
|
|
This module aims to supplant both L<Moose>'s built-in type system (see
|
|
L<Moose::Util::TypeConstraints> aka MUTC) and L<MooseX::Types>, which attempts
|
|
to patch some of the holes in the Moose built-in type design.
|
|
|
|
Here are some of the salient differences:
|
|
|
|
=over 4
|
|
|
|
=item * Types names are strings, but they're not global
|
|
|
|
Unlike Moose and MooseX::Types, type names are always local to the current
|
|
package. There is no possibility of name collision between different modules,
|
|
so you can safely use short type names.
|
|
|
|
Unlike MooseX::Types, types are strings, so there is no possibility of
|
|
colliding with existing class or subroutine names.
|
|
|
|
=item * No type auto-creation
|
|
|
|
Types are always retrieved using the C<t()> subroutine. If you pass an unknown
|
|
name to this subroutine it dies. This is different from Moose and
|
|
MooseX::Types, which assume that unknown names are class names.
|
|
|
|
=item * Anon types are explicit
|
|
|
|
With L<Moose> and L<MooseX::Types>, you use the same subroutine, C<subtype()>,
|
|
to declare both named and anonymous types. With Specio, you use C<declare()> for
|
|
named types and C<anon()> for anonymous types.
|
|
|
|
=item * Class and object types are separate
|
|
|
|
Moose and MooseX::Types have C<class_type> and C<duck_type>. The former type
|
|
requires an object, while the latter accepts a class name or object.
|
|
|
|
With Specio, the distinction between accepting an object versus object or
|
|
class is explicit. There are six declaration helpers, C<object_can_type>,
|
|
C<object_does_type>, C<object_isa_type>, C<any_can_type>, C<any_does_type>,
|
|
and C<any_isa_type>.
|
|
|
|
=item * Overloading support is baked in
|
|
|
|
Perl's overloading is quite broken but ignoring it makes Moose's type system
|
|
frustrating to use in many cases.
|
|
|
|
=item * Types can either have a constraint or inline generator, not both
|
|
|
|
Moose and MooseX::Types types can be defined with a subroutine reference as
|
|
the constraint, an inline generator subroutine, or both. This is purely for
|
|
backwards compatibility, and it makes the internals more complicated than they
|
|
need to be.
|
|
|
|
With Specio, a constraint can have I<either> a subroutine reference or an
|
|
inline generator, not both.
|
|
|
|
=item * Coercions can be inlined
|
|
|
|
I simply never got around to implementing this in Moose.
|
|
|
|
=item * No crazy coercion features
|
|
|
|
Moose has some bizarre (and mostly) undocumented features relating to
|
|
coercions and parameterizable types. This is a misfeature.
|
|
|
|
=back
|
|
|
|
=head1 OPTIONAL PREREQS
|
|
|
|
There are several optional prereqs that if installed will make this
|
|
distribution better in some way.
|
|
|
|
=over 4
|
|
|
|
=item * L<Ref::Util>
|
|
|
|
Installing this will speed up a number of type checks for built-in types.
|
|
|
|
=item * L<XString>
|
|
|
|
If this is installed it will be loaded instead of the L<B> module if you have
|
|
Perl 5.10 or greater. This module is much more memory efficient than loading
|
|
all of L<B>.
|
|
|
|
=item * L<Sub::Util> or L<Sub::Name>
|
|
|
|
If one of these is installed then stack traces that end up in Specio code will
|
|
have much better subroutine names for any frames.
|
|
|
|
=back
|
|
|
|
=head1 WHY THE NAME?
|
|
|
|
This distro was originally called "Type", but that's an awfully generic top
|
|
level namespace. Specio is Latin for for "look at" and "spec" is the root for
|
|
the word "species". It's short, relatively easy to type, and not used by any
|
|
other distro.
|
|
|
|
=head1 LONG-TERM PLANS
|
|
|
|
Eventually I'd like to see this distro replace Moose's internal type system,
|
|
which would also make MooseX::Types obsolete.
|
|
|
|
=head1 SUPPORT
|
|
|
|
Bugs may be submitted at L<https://github.com/houseabsolute/Specio/issues>.
|
|
|
|
I am also usually active on IRC as 'autarch' on C<irc://irc.perl.org>.
|
|
|
|
=head1 SOURCE
|
|
|
|
The source code repository for Specio can be found at L<https://github.com/houseabsolute/Specio>.
|
|
|
|
=head1 DONATIONS
|
|
|
|
If you'd like to thank me for the work I've done on this module, please
|
|
consider making a "donation" to me via PayPal. I spend a lot of free time
|
|
creating free software, and would appreciate any support you'd care to offer.
|
|
|
|
Please note that B<I am not suggesting that you must do this> in order for me
|
|
to continue working on this particular software. I will continue to do so,
|
|
inasmuch as I have in the past, for as long as it interests me.
|
|
|
|
Similarly, a donation made in this way will probably not make me work on this
|
|
software much more, unless I get so many donations that I can consider working
|
|
on free software full time (let's all have a chuckle at that together).
|
|
|
|
To donate, log into PayPal and send money to autarch@urth.org, or use the
|
|
button at L<https://www.urth.org/fs-donation.html>.
|
|
|
|
=head1 AUTHOR
|
|
|
|
Dave Rolsky <autarch@urth.org>
|
|
|
|
=head1 CONTRIBUTORS
|
|
|
|
=for stopwords Chris White cpansprout Graham Knop Karen Etheridge
|
|
|
|
=over 4
|
|
|
|
=item *
|
|
|
|
Chris White <chrisw@leehayes.com>
|
|
|
|
=item *
|
|
|
|
cpansprout <cpansprout@gmail.com>
|
|
|
|
=item *
|
|
|
|
Graham Knop <haarg@haarg.org>
|
|
|
|
=item *
|
|
|
|
Karen Etheridge <ether@cpan.org>
|
|
|
|
=back
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
This software is Copyright (c) 2012 - 2020 by Dave Rolsky.
|
|
|
|
This is free software, licensed under:
|
|
|
|
The Artistic License 2.0 (GPL Compatible)
|
|
|
|
The full text of the license can be found in the
|
|
F<LICENSE> file included with this distribution.
|
|
|
|
=cut
|