Iterator-Flex

 view release on metacpan or  search on metacpan

lib/Iterator/Flex/Base.pm  view on Meta::CPAN

package Iterator::Flex::Base;

# ABSTRACT: Iterator object

use v5.28;
use strict;
use warnings;

use experimental qw( signatures postderef declared_refs );

our $VERSION = '0.33';

use Ref::Util;
use Scalar::Util;
use List::Util;
use Role::Tiny       ();
use Role::Tiny::With ();
use Module::Runtime  ();

use Iterator::Flex::Utils qw (
  :default
  :ExhaustionActions
  :GeneralParameters
  :RegistryIndices
  :IterAttrs
  :IterStates
  :InterfaceParameters
  :SignalParameters
  load_role
  throw_failure
);

use namespace::clean;

use overload
  '<>'     => sub ( $self, $, $ ) { &{$self}() },
  fallback => 0,
  bool     => sub { 1 },

  # these are required for the perldb to not barf
  # see https://github.com/Perl/perl5/issues/23486
  eq    => sub { 0 },
  q{""} => sub { q{} },
  ;

# We separate constructor parameters into two categories:
#
#  1. those that are used to construct the iterator
#  2. those that specify what happens when the iterator signals exhaustion
#
#  Category #2 may be expanded. Category #2 parameters are *not* passed
#  to the iterator class construct* routines


sub new ( $class, $state = undef, $general = {} ) {
    return $class->new_from_state( $state, $general );
}

sub new_from_state ( $class, $state, $general ) {
    return $class->new_from_attrs( $class->construct( $state ), $general );
}

sub new_from_attrs ( $class, $in_ipar = {}, $in_gpar = {} ) {    ## no critic (ExcessComplexity)

    my %ipar = $in_ipar->%*;
    my %gpar = $in_gpar->%*;

    $class->_validate_interface_pars( \%ipar );
    $class->_validate_signal_pars( \%gpar );

    my @roles = ( delete( $ipar{ +_ROLES } ) // [] )->@*;

    $gpar{ +ERROR } //= [THROW];
    $gpar{ +ERROR } = [ $gpar{ +ERROR } ]
      unless Ref::Util::is_arrayref( $gpar{ +ERROR } );

    if ( $gpar{ +ERROR }[0] eq THROW ) {
        push @roles, 'Error::Throw';
    }
    else {
        throw_failure( q{unknown specification of iterator error signaling behavior:}, $gpar{ +ERROR }[0] );
    }

    my $exhaustion_action = $gpar{ +EXHAUSTION } // [ ( +RETURN ) => undef ];

    my @exhaustion_action
      = Ref::Util::is_arrayref( $exhaustion_action )
      ? ( $exhaustion_action->@* )
      : ( $exhaustion_action );

    $gpar{ +EXHAUSTION } = \@exhaustion_action;

    if ( $exhaustion_action[0] eq RETURN ) {
        push @roles, 'Exhaustion::Return';
    }
    elsif ( $exhaustion_action[0] eq THROW ) {

        push @roles,
          @exhaustion_action > 1 && $exhaustion_action[1] eq PASSTHROUGH
          ? 'Exhaustion::PassthroughThrow'
          : 'Exhaustion::Throw';
    }
    else {
        throw_failure( parameter => "unknown exhaustion action: $exhaustion_action[0]" );
    }

    if ( defined( my $par = $ipar{ +METHODS } ) ) {

        require Iterator::Flex::Method;

        throw_failure( parameter => q{value for methods parameter must be a hash reference} )
          unless Ref::Util::is_hashref( $par );

        for my $name ( keys $par->%* ) {

            my $code = $par->{$name};

            throw_failure( parameter => "value for 'methods' parameter key '$name' must be a code reference" )
              unless Ref::Util::is_coderef( $code );

            # create role for the method
            my $role = eval { Iterator::Flex::Method::Maker( $name, name => $name ) };

            if ( $@ ne q{} ) {
                my $error = $@;
                die $error
                  unless Ref::Util::is_blessed_ref( $error )
                  && $error->isa( 'Iterator::Flex::Failure::RoleExists' );
                $role = $error->payload;
            }

            push @roles, q{+} . $role;    # need '+', as these are fully qualified role module names.
        }
    }

    @roles = map { load_role( $_, $class->_role_namespaces ) } @roles;
    $class = Role::Tiny->create_class_with_roles( $class, @roles );

    unless ( $class->can( '_construct_next' ) ) {
        throw_failure(
            class => "Constructed class '$class' does not provide the required _construct_next method\n" );
    }

    unless ( $class->does( 'Iterator::Flex::Role::State' ) ) {
        throw_failure( class => "Constructed class '$class' does not provide a State role\n" );
    }

    $ipar{ +_NAME } //= $class;

    my $self = bless $class->_construct_next( \%ipar, \%gpar ), $class;

    throw_failure(
        parameter => q{attempt to register an iterator subroutine which has already been registered.} )
      if exists $REGISTRY{ refaddr $self };

    my $regentry = $REGISTRY{ refaddr $self } = [];

    # convert to arrays. some of the parameter values are weak
    # references so make sure we don't unweaken them
    my @ipar;
    for my $key ( keys %ipar ) {
        $ipar[ $RegIterationIndexMap{$key} ] = $ipar{$key};
        Scalar::Util::weaken $ipar[ $RegIterationIndexMap{$key} ]
          if Ref::Util::is_ref( $ipar{$key} )
          && Scalar::Util::isweak( $ipar{$key} );
    }
    my @gpar;
    for my $key ( keys %gpar ) {
        $gpar[ $RegGeneralParameterIndexMap{$key} ] = $gpar{$key};
        Scalar::Util::weaken $gpar[ $RegGeneralParameterIndexMap{$key} ]
          if Ref::Util::is_ref( $gpar{$key} )
          && Scalar::Util::isweak( $gpar{$key} );
    }

    $regentry->[REG_ITERATOR] = \@ipar;
    $regentry->[REG_GENERAL]  = \@gpar;

    $self->_clear_state;

    return $self;
}

sub _validate_interface_pars ( $class, $pars ) {
    state %InterfaceParameters = {}->%{ +INTERFACE_PARAMETER_VALUES };

    my @bad = grep { !exists $InterfaceParameters{$_} } keys $pars->%*;

    throw_failure( parameter => "unknown interface parameters: @{[ join ', ', @bad ]}" )
      if @bad;

    throw_failure( parameter => "@{[ _ROLES ]}  must be an arrayref" )
      if defined $pars->{_ROLES} && !Ref::Util::is_arrayref( $pars->{ +_ROLES } );

    if ( defined( my $par = $pars->{ +_DEPENDS } ) ) {
        $pars->{ +_DEPENDS } = $par = [$par] unless Ref::Util::is_arrayref( $par );
        throw_failure( parameter => "dependency #$_ is not an iterator object" )
          unless List::Util::all { $class->_is_iterator( $_ ) } $par->@*;
    }

    return;
}

sub _validate_signal_pars ( $class, $pars ) {
    state %SignalParameters = {}->%{ +SIGNAL_PARAMETER_VALUES };
    my @bad = grep { !exists $SignalParameters{$_} } keys $pars->%*;

    throw_failure( parameter => "unknown signal parameters: @{[ join ', ', @bad ]}" )
      if @bad;
}


sub DESTROY ( $self ) {

    if ( defined $self ) {
        delete $REGISTRY{ refaddr $self };
    }
}

sub _name ( $self ) {
    $REGISTRY{ refaddr $self }[REG_ITERATOR][REG_ITER__NAME];
}






















# TODO: this is too restrictive. It should allow simple coderefs, or
# things with a next or __next__.

sub _is_iterator ( $, $obj ) {
    return Ref::Util::is_blessed_ref( $obj ) && $obj->isa( __PACKAGE__ );
}













sub __iter__ ( $self ) {
    return $REGISTRY{ refaddr $self }[REG_ITERATOR][REG_ITER_NEXT];
}













sub may ( $self, $meth ) {

    my \@attributes = $REGISTRY{ refaddr $self }[REG_ITERATOR];
    my $may         = $attributes[REG_ITER_MAY_METHOD] //= {};

    return $may->{"_may_$meth"}
      //= defined $attributes[REG_ITER__DEPENDS]
      ? !List::Util::first { !$_->may( $meth ) } $attributes[REG_ITER__DEPENDS]->@*
      : defined $attributes[ $RegIterationIndexMap{$meth} ];
}
















sub _namespaces {
    return 'Iterator::Flex';
}

lib/Iterator/Flex/Base.pm  view on Meta::CPAN











sub chunk ( $self, $pars = {} ) {
    require Iterator::Flex::Chunk;
    Iterator::Flex::Chunk->new( $self, $pars );
}

sub ichunk;
*ichunk = \&chunk;

sub batch;
*batch = \&chunk;














sub drain ( $self, $n = undef ) {

    my @values;

    eval {
        if ( $n ) {
            while ( $n-- ) {
                push @values, $self->next;
                if ( $self->is_exhausted ) {
                    pop @values;
                    last;
                }
            }
        }
        else {
            while ( 1 ) {
                push @values, $self->next;
                if ( $self->is_exhausted ) {
                    pop @values;
                    last;
                }
            }
        }
        1;
    } or do {
        die $@
          unless Ref::Util::is_blessed_ref( $@ )
          && $@->isa( 'Iterator::Flex::Failure::Exhausted' );
    };

    return \@values;
}









sub flatten ( $self, $pars = {} ) {
    require Iterator::Flex::Flatten;
    return Iterator::Flex::Flatten->new( $self, $pars );
}

sub iflatten;
*iflatten = \*flatten;










sub foreach ( $self, $code ) {    ## no critic (BuiltinHomonyms)

    if ( $self->throws_on_exhaustion ) {
        eval {
            local $_;    ## no critic (InitializationForLocalVars)
            while ( $_ = $self->() ) { $code->() }
            1;
        } or do {
            die $@
              unless Ref::Util::is_blessed_ref( $@ )
              && $@->isa( 'Iterator::Flex::Failure::Exhausted' );
        };
    }
    elsif ( $self->returns_on_exhaustion ) {

        # optimize for when sentinel is the undefined value
        if ( !defined $self->sentinel ) {
            local $_;    ## no critic (InitializationForLocalVars)
            $code->() while defined( $_ = $self->() );
        }

        # yeah, this is too slow.  should adapt the logic in Wrap::Return.
        else {
            local $_;                          ## no critic (InitializationForLocalVars)
            $_ = $self->();
            until ( $self->is_exhausted ) {    ## no critic (UntilBlock)
                $code->();
                $_ = $self->();
            }
        }
    }
}










sub gather ( $self, $code, $pars = {} ) {
    require Iterator::Flex::Gather;
    Iterator::Flex::Gather->new( $code, $self, $pars );
}

sub igather;
*igather = \&gather;









sub grep ( $self, $code, $pars = {} ) {    ## no critic (BuiltinHomonyms)

    require Iterator::Flex::Grep;
    Iterator::Flex::Grep->new( $code, $self, $pars );
}

sub igrep;
*igrep = \&grep;






( run in 0.886 second using v1.01-cache-2.11-cpan-39bf76dae61 )