NAME
Newtype - Perl implementation of an approximation for Haskell's newtype
SYNOPSIS
package MyClass;
use HTTP::Tiny ();
use Newtype HttpTiny => { inner => 'HTTP::Tiny' };
use Moo;
has ua => (
is => 'ro',
isa => HttpTiny(),
coerce => 1,
);
DESCRIPTION
This module allows you to create a new type which is a subclass of an existing type.
Why?
Well maybe you want to add some new methods to the new type:
use HTTP::Tiny ();
use Newtype HttpTiny => {
inner => 'HTTP::Tiny',
methods => {
'post_or_get' => sub {
my $self = shift;
my $res = $self->post( @_ );
return $res if $res->{success};
return $self->get( @_ );
},
};
Or maybe you need to differentiate between two different kinds of things which are otherwise the same class.
use Newtype (
SecureUA => { inner => 'HTTP::Tiny' },
InsecureUA => { inner => 'HTTP::Tiny' },
);
...;
my $ua = InsecureUA( HTTP::Tiny->new );
...;
if ( $ua->isa(SecureUA) ) {
...;
}
Newtype can also create new types which "inherit" from Perl builtins.
use Types::Common qw( ArrayRef PositiveInt );
use Newtype Numbers => { inner => ArrayRef[PositiveInt] };
my $nums = Numbers( [] );
$nums->push( 1 );
$nums->push( 2 );
$nums->push( -1 ); # dies
See Hydrogen for the list of available methods for builtins.
Newtypes which inherit from builtins use overloading to attempt to provide transparency.
Although there will be exceptions to this general rule of thumb (especially if your newtype is inheriting from a Perl builtin), you can think of things like this: if you create a type NewFoo from existing type Foo, then instances of NewFoo should be accepted everywhere instances of Foo are. But instances of Foo will not be automatically accepted where instances of NewFoo are.
Creating a newtype
The general form for creating newtypes is:
use Newtype $typename => {
inner => $inner_type,
%other_options,
};
The inner type is required, and must be either a string class name or a Type::Tiny type constraint indicating what type of thing you want to wrap.
Other supported options are:
methods
-
A hashref of methods to add to the newtype. Keys are the method names. Values are coderefs.
kind
-
This allows you to give Newtype a hint for how to delegate to the inner value. Supported kinds (case-sensitive) are: Array, Bool, Code, Counter, Hash, Number, Object, and String. Usually Newtype will be able to guess based on
inner
though.
Creating values belonging to the newtype
When you import a newtype Foo, you import a function Foo()
into your namespace. You can create instances of the newtype using:
Foo( $inner_value )
Where $inner_value
is an instance of the type you're wrapping.
For example:
use HTTP::Tiny;
use Newtype UA => { inner => 'HTTP::Tiny' };
my $ua = UA( HTTP::Tiny->new );
Note: you also get is_Foo
, assert_Foo
, and to_Foo
functions imported! is_Foo( $x )
checks if $x
is a Foo object and returns a boolean. assert_Foo( $x )
does the same, but dies if it fails. to_Foo( $x )
attempts to coerce $x
to a Foo object.
Integration with Moose, Mouse, and Moo
If your imported newtype is Foo, then calling Foo()
with no arguments will return a Type::Tiny type constraint for the newtype.
use HTTP::Tiny;
use Newtype UA => { inner => 'HTTP::Tiny' };
use Moo;
has my_ua => ( is => 'ro', isa => UA() );
Now people instantiating your class will need to pass you a wrapped HTTP::Tiny object instead of passing a normal HTTP::Tiny object. You may wish to allow them to pass you a normal HTTP::Tiny object though. That should be easy with coercions:
has my_ua => ( is => 'ro', isa => UA(), coerce => 1 );
Accessing the inner value
You can access the original wrapped value using the INNER
method.
my $ua = UA( HTTP::Tiny->new );
my $http_tiny_object = $ua->INNER;
Introspection
If your newtype is called MyNewtype, then you can introspect it using a few methods:
MyNewtype->class
-
The class powering the newtype.
MyNewtype->inner_type
-
The type constraint for the inner value.
MyNewtype->kind
-
The kind of delegation being used.
The object returned by MyNewtype()
is also a Type::Tiny object, so you can call any method from Type::Tiny, such as MyNewtype->check( $value )
or MyNewtype->coerce( $value )
.
EXAMPLES
Using newtypes instead of named parameters
Let's say you have a function like this:
sub run_processes {
my ( $runtime_processes, $startup_processes, $shutdown_processes ) = @_;
$_->() for @$startup_processes;
$_->() for @$runtime_processes;
$_->() for @$shutdown_processes;
}
This function takes three arrayrefs of coderefs. It's very easy for the caller to forget what order to pass them in, and potentially pass them in the wrong order.
Let's bring some newtypes into the mix:
use feature 'state';
use Types::Common qw( CodeRef, ArrayRef );
use Type::Params qw( signature );
use Newtype (
StartupProcessList => { inner => ArrayRef[CodeRef] },
RuntimeProcessList => { inner => ArrayRef[CodeRef] },
ShutdownProcessList => { inner => ArrayRef[CodeRef] },
);
sub run_processes {
state $sig = signature positional => [
RuntimeProcessList->no_coercions,
StartupProcessList->no_coercions,
ShutdownProcessList->no_coercions,
];
my ( $runtime_processes, $startup_processes, $shutdown_processes ) = &$sig;
$_->() for @$startup_processes;
$_->() for @$runtime_processes;
$_->() for @$shutdown_processes;
}
Now your function no longer accepts bare arrayrefs. Instead the caller needs to convert their arrayrefs into your newtype. The need to call your function like this:
run_processes(
RuntimeProcessList( \@coderefs1 ),
StartupProcessList( \@coderefs2 ),
ShutdownProcessList( \@coderefs3 ),
);
If they try to pass the lists in the wrong order, they'll get a type constraint error.
Exporting the RuntimeProcessList
, StartupProcessList
, and ShutdownProcessList
functions to your caller is left as an exercise for the reader!
BUGS
Please report any bugs to https://github.com/tobyink/p5-newtype/issues.
SEE ALSO
Type::Tiny::Class, Subclass::Of.
https://wiki.haskell.org/Newtype.
AUTHOR
Toby Inkster <tobyink@cpan.org>.
COPYRIGHT AND LICENCE
This software is copyright (c) 2022 by Toby Inkster.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
DISCLAIMER OF WARRANTIES
THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.