]> git.vpit.fr Git - perl/modules/Sub-Prototype-Util.git/blobdiff - lib/Sub/Prototype/Util.pm
_ shouldn't push elements in flatten, but skip them
[perl/modules/Sub-Prototype-Util.git] / lib / Sub / Prototype / Util.pm
index f090f87be84c8460f2bc030e535d097b1868248e..a92647f6a4d224985ff72ac11dea3fd75ed980ca 100644 (file)
@@ -8,29 +8,33 @@ use Scalar::Util qw/reftype/;
 
 =head1 NAME
 
-Sub::Prototype::Util - Prototypes-related utility routines.
+Sub::Prototype::Util - Prototype-related utility routines.
 
 =head1 VERSION
 
-Version 0.01
+Version 0.08
 
 =cut
 
-our $VERSION = '0.01';
+use vars qw/$VERSION/;
+
+$VERSION = '0.08';
 
 =head1 SYNOPSIS
 
-    use Sub::Prototype::Util qw/flatten recall/;
+    use Sub::Prototype::Util qw/flatten wrap recall/;
 
     my @a = qw/a b c/;
     my @args = ( \@a, 1, { d => 2 }, undef, 3 );
 
     my @flat = flatten '\@$;$', @args; # ('a', 'b', 'c', 1, { d => 2 })
     recall 'CORE::push', @args; # @a contains 'a', 'b', 'c', 1, { d => 2 }, undef, 3
+    my $splice = wrap 'CORE::splice';
+    my @b = $splice->(\@a, 4, 2); # @a is now ('a', 'b', 'c', 1, 3) and @b is ({ d => 2 }, undef)
 
 =head1 DESCRIPTION
 
-Prototypes are evil, but sometimes you just have to bear with them, especially when messing with core functions. This module provides several utilities aimed at faciliting "overloading" of prototyped functions.
+Prototypes are evil, but sometimes you just have to bear with them, especially when messing with core functions. This module provides several utilities aimed at facilitating "overloading" of prototyped functions.
 
 They all handle C<5.10>'s C<_> prototype.
 
@@ -39,6 +43,7 @@ They all handle C<5.10>'s C<_> prototype.
 =cut
 
 my %sigils = qw/SCALAR $ ARRAY @ HASH % GLOB * CODE &/;
+my %reftypes = reverse %sigils;
 
 sub _check_ref {
  my ($a, $p) = @_;
@@ -67,19 +72,23 @@ sub flatten {
   if ($1) {
    my $a = shift;
    my $r = _check_ref $a, $p;
-   my %deref = (
-    SCALAR => sub { push @args, $$a },
-    ARRAY  => sub { push @args, @$a },
-    HASH   => sub { push @args, %$a },
-    GLOB   => sub { push @args, *$a },
-    CODE   => sub { push @args, &$a }
-   );
-   $deref{$r}->();
+   push @args, $r eq 'SCALAR'
+               ? $$a
+               : ($r eq 'ARRAY'
+                  ? @$a
+                  : ($r eq 'HASH'
+                     ? %$a
+                     : ($r eq 'GLOB'
+                        ? *$a
+                        : &$a # _check_ref ensures this must be a code ref
+                       )
+                    )
+                 );
   } elsif ($p =~ /[\@\%]/) {
    push @args, @_;
    last;
   } elsif ($p eq '_') {
-   push @args, $_;
+   shift; # without prototype, this argument wouldn't have been passed
   } else {
    push @args, shift;
   }
@@ -87,75 +96,184 @@ sub flatten {
  return @args;
 }
 
-=head2 C<recall $name, @args>
+=head2 C<wrap $name, %opts>
 
-Calls the function C<$name> with the prototyped argument list C<@args>. That is, C<@args> should be what C<@_> is when you define a subroutine with the same prototype as C<$name>. For example,
+Generates a wrapper that calls the function C<$name> with a prototyped argument list. That is, the wrapper's arguments should be what C<@_> is when you define a subroutine with the same prototype as C<$name>.
 
-    my $a = [ ];
-    recall 'CORE::push', $a, 1, 2, 3;
+    my $a = [ 0 .. 2 ];
+    my $push = wrap 'CORE::push';
+    $push->($a, 3, 4); # returns 3 + 2 = 5 and $a now contains 0 .. 4
+
+You can force the use of a specific prototype. In this case, C<$name> must be a hash reference that holds exactly one key / value pair, the key being the function name and the value the prototpye that should be used to call it.
+
+    my $push = wrap { 'CORE::push' => '\@$' }; # only pushes 1 arg
+
+Others arguments are seen as key / value pairs that are meant to tune the code generated by L</wrap>. Valid keys are :
+
+=over 4
+
+=item C<< ref => $func >>
+
+Specifies the function used in the generated code to test the reference type of scalars. Defaults to C<'ref'>. You may also want to use C<Scalar::Util::reftype>.
+
+=item C<< wrong_ref => $code >>
+
+The code executed when a reference of incorrect type is encountered. The result of this snippet is also the result of the generated code, hence it defaults to C<'undef'>. It's a good place to C<croak> or C<die> too.
+
+=item C<< sub => $bool >>
+
+Encloses the code into a C<sub { }> block. Default is true.
 
-will call C<push @$a, 1, 2, 3> and so fill the arrayref C<$a> with C<1, 2, 3>. This is especially needed for core functions because you can't C<goto> into them.
+=item C<< compile => $bool >>
+
+Makes L</wrap> compile the code generated and return the resulting code reference. Be careful that in this case C<ref> must be a fully qualified function name. Defaults to true, but turned off when C<sub> is false.
+
+=back
+
+For example, this allows you to recall into C<CORE::grep> and C<CORE::map> by using the C<\&@> prototype :
+
+    my $grep = wrap { 'CORE::grep' => '\&@' };
+    sub mygrep (&@) { $grep->(@_) } # the prototypes are intentionally different
 
 =cut
 
-sub recall {
- my ($name, @a) = @_;
- croak 'Wrong subroutine name' unless $name;
- $name =~ s/^\s+//;
- $name =~ s/[\s\$\@\%\*\&;].*//;
- my $proto = prototype $name;
- my @args;
- if (defined $proto) {
-  my $i = 0;
-  while ($proto =~ /(\\?)(\[[^\]]+\]|[^\];])/g) {
-   my $p = $2;
-   if ($1) {
-    my $r = _check_ref $a[$i], $p;
-    push @args, join '', $sigils{$r}, '{$a[', $i, ']}';
-   } elsif ($p =~ /[\@\%]/) {
-    push @args, join '', '@a[', $i, '..', (@a - 1), ']';
-    last;
-   } elsif ($p =~ /\&/) {
-    push @args, 'sub{&{$a[' . $i . ']}}';
-   } elsif ($p eq '_') {
-    push @args, '$_';
+sub _wrap {
+ my ($name, $proto, $i, $args, $cr, $opts) = @_;
+ if ($proto =~ /(\\?)(\[[^\]]+\]|[^\];])(.*)/g) {
+  my ($ref, $p) = ($1, $2);
+  $proto = $3;
+  $p = $1 if $p =~ /^\[([^\]]+)\]/;
+  my $cur = '$_[' . $i . ']';
+  if ($ref) {
+   if (length $p > 1) {
+    return 'my $r = ' . $opts->{ref} . '(' . $cur . '); ' 
+           . join ' els',
+              map( {
+               "if (\$r eq '" . $reftypes{$_} ."') { "
+               . _wrap($name, $proto, ($i + 1),
+                              $args . $_ . '{' . $cur . '}, ',
+                              $cr, $opts)
+               . ' }'
+              } split //, $p),
+              'e { ' . $opts->{wrong_ref} . ' }'
+   } else {
+    $args .= $p . '{' . $cur . '}, ';
+   }
+  } elsif ($p =~ /[\@\%]/) {
+   $args .= '@_[' . $i . '..$#_]';
+  } elsif ($p =~ /\&/) {
+   my %h = do { my $c; map { $_ => $c++ } @$cr };
+   my $j;
+   if (not exists $h{$i}) {
+    push @$cr, $i;
+    $j = $#{$cr};
    } else {
-    push @args, '$a[' . $i . ']';
+    $j = int $h{$i};
    }
-   ++$i; 
+   $args .= 'sub{&{$c[' . $j . ']}}, ';
+  } elsif ($p eq '_') {
+   $args .= '((@_ > ' . $i . ') ? ' . $cur . ' : $_), ';
+  } else {
+   $args .= $cur . ', ';
   }
+  return _wrap($name, $proto, ($i + 1), $args, $cr, $opts);
  } else {
-  @args = map '$a[' . $_ . ']', 0 .. @a - 1;
+  $args =~ s/,\s*$//;
+  return $name . '(' . $args . ')';
  }
- my @ret = eval $name . '(' . join(',', @args) . ');';
+}
+
+sub _check_name {
+ my $name = $_[0];
+ croak 'No subroutine specified' unless $name;
+ my $proto;
+ my $r = ref $name;
+ if (!$r) {
+  $proto = prototype $name;
+ } elsif ($r eq 'HASH') {
+  croak 'Forced prototype hash reference must contain exactly one key/value pair' unless keys %$name == 1;
+  ($name, $proto) = %$name;
+ } else {
+  croak 'Unhandled ' . $r . ' reference as first argument';
+ }
+ $name =~ s/^\s+//;
+ $name =~ s/[\s\$\@\%\*\&;].*//;
+ return $name, $proto;
+}
+
+sub wrap {
+ my ($name, $proto) = _check_name shift;
+ croak 'Optional arguments must be passed as key => value pairs' if @_ % 2;
+ my %opts = @_;
+ $opts{ref}     ||= 'ref';
+ $opts{sub}       = 1       if not defined $opts{sub};
+ $opts{compile}   = 1       if not defined $opts{compile} and $opts{sub};
+ $opts{wrong_ref} = 'undef' if not defined $opts{wrong_ref};
+ my @cr;
+ my $call;
+ if (defined $proto) {
+  $call = _wrap $name, $proto, 0, '', \@cr, \%opts;
+ } else {
+  $call = _wrap $name, '', 0, '@_';
+ }
+ if (@cr) {
+  $call = 'my @c; '
+        . join('', map { 'push @c, $_[' . $_ . ']; ' } @cr)
+        . $call
+ }
+ $call = '{ ' . $call . ' }';
+ $call = 'sub ' . $call if $opts{sub};
+ if ($opts{compile}) {
+  $call = eval $call;
+  croak $@ if $@;
+ }
+ return $call;
+}
+
+=head2 C<recall $name, @args>
+
+Calls the function C<$name> with the prototyped argument list C<@args>. That is, C<@args> should be what C<@_> is when you define a subroutine with the same prototype as C<$name>. You can still force the prototype by passing C<< { $name => $proto } >> as the first argument.
+
+    my $a = [ ];
+    recall { 'CORE::push' => '\@$' }, $a, 1, 2, 3; # $a just contains 1
+
+It's implemented in terms of L</wrap>, and hence calls C<eval> at each run.
+If you plan to recall several times, consider using L</wrap> instead.
+
+=cut
+
+sub recall {
+ my $wrap = eval { wrap shift };
  croak $@ if $@;
- return @ret;
+ return $wrap->(@_);
 }
 
 =head1 EXPORT
 
-The functions L</flatten> and L</recall> are only exported on request, either by providing their name or by the C<':consts'> and C<':all'> tags.
+The functions L</flatten>, L</wrap> and L</recall> are only exported on request, either by providing their name or by the C<':funcs'> and C<':all'> tags.
 
 =cut
 
 use base qw/Exporter/;
 
-our @EXPORT         = ();
-our %EXPORT_TAGS    = (
- 'funcs' =>  [ qw/flatten recall/ ]
+use vars qw/@EXPORT @EXPORT_OK %EXPORT_TAGS/;
+
+@EXPORT             = ();
+%EXPORT_TAGS        = (
+ 'funcs' =>  [ qw/flatten wrap recall/ ]
 );
-our @EXPORT_OK      = map { @$_ } values %EXPORT_TAGS;
+@EXPORT_OK          = map { @$_ } values %EXPORT_TAGS;
 $EXPORT_TAGS{'all'} = [ @EXPORT_OK ];
 
 =head1 DEPENDENCIES
 
-L<Carp> (core module since perl 5), L<Scalar::Util> (since 5.7.3).
+L<Carp>, L<Exporter> (core modules since perl 5), L<Scalar::Util> (since 5.7.3).
 
 =head1 AUTHOR
 
 Vincent Pit, C<< <perl at profvince.com> >>, L<http://www.profvince.com>.
 
-You can contact me by mail or on #perl @ FreeNode (vincent or Prof_Vince).
+You can contact me by mail or on C<irc.perl.org> (vincent).
 
 =head1 BUGS