The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

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.