package Function::Return; use v5.14.0; use warnings; our $VERSION = "0.15"; use Attribute::Handlers; use B::Hooks::EndOfScope; use Scope::Upper (); use Sub::Meta; use Sub::Meta::Library; use Sub::Meta::Finder::FunctionParameters; use namespace::autoclean; my @RETURN_ARGS; my %NO_CHECK; sub import { my $class = shift; my %args = @_; my $pkg = $args{pkg} ? $args{pkg} : scalar caller; $NO_CHECK{$pkg} = !!$args{no_check} if exists $args{no_check}; { # allow importing package to use attribute no strict qw(refs); my $MODIFY_CODE_ATTRIBUTES = \&Attribute::Handlers::UNIVERSAL::MODIFY_CODE_ATTRIBUTES; *{"${pkg}::MODIFY_CODE_ATTRIBUTES"} = $MODIFY_CODE_ATTRIBUTES; *{"${pkg}::_ATTR_CODE_Return"} = $class->can('Return'); } # # How to install meta information # 1. At the BEGIN phase, write down the meta information via the `Return` attribute. # 2. At the compile phase, install the meta information in bulk via this `import` subroutine. # # In short, # once Function::Return#import is compiled, the meta-information can be retrieved. # # The Reason Why? # # First NG CASE: # At the **CHECK** phase, write down the meta information via the Return attribute. (Attribute::Handler's default case) # Then, cannot support lazy load. # Ref: case_lazy_load.t # # Second NG CASE: # At the compile phase, install the meta information in **each** via this **Return** attribute. # Then, unable to retrieve meta information for Function::Return from places that are compiled before the Return attribute. # Ref: case_load_and_get_meta.t # on_scope_end { while (my $args = shift @RETURN_ARGS) { my ($pkg, $sub, $types) = @$args; my $no_check = exists $NO_CHECK{$pkg} ? $NO_CHECK{$pkg} : ($ENV{FUNCTION_RETURN_NO_CHECK}//0); if ($no_check) { $class->_register_submeta($pkg, $sub, $types); } else { $class->_register_submeta_and_install($pkg, $sub, $types); } } }; return; } sub Return :ATTR(CODE,BEGIN) { my $class = __PACKAGE__; my ($pkg, undef, $sub, undef, $types) = @_; $types //= []; push @RETURN_ARGS => [$pkg, $sub, $types]; return; } sub meta { my ($sub) = @_; Sub::Meta::Library->get($sub); } sub wrap_sub { my ($class, $sub, $types) = @_; my $meta = Sub::Meta->new(sub => $sub); my $shortname = $meta->subname; { # check type my $file = $meta->file; my $line = $meta->line; for my $type (@$types) { for (qw/check get_message/) { die "Invalid type: $type. require `$_` method at $file line $line.\n" unless $type->can($_) } } } my @src; push @src => sprintf('_croak "Required list context in fun %s because of multiple return values function" if !wantarray;', $shortname) if @$types > 1; # force LIST context. push @src => 'my @ret = &Scope::Upper::uplevel($sub, @_, &Scope::Upper::CALLER(0));'; # return Empty List push @src => 'return if !@ret;' if @$types == 0; # check count push @src => sprintf(q|_croak qq!Too few return values for fun %s (expected %s, got @{[map { defined $_ ? $_ : 'undef' } @ret]})! if @ret < %d;|, $shortname, "@$types", scalar @$types) if @$types > 0; push @src => sprintf(q|_croak qq!Too many return values for fun %s (expected %s, got @{[map { defined $_ ? $_ : 'undef' } @ret]})! if @ret > %d;|, $shortname, "@$types", scalar @$types); # type check for my $i (0 .. $#$types) { push @src => sprintf(q|_croak qq!Invalid return in fun %s: return %d: @{[$types->[%d]->get_message($ret[%d])]}! unless $types->[%d]->check($ret[%d]);|, $shortname, $i, $i, $i, $i,$i) } push @src => 'return @ret;' if @$types > 1; push @src => 'return $ret[0];' if @$types == 1; my $src = join "\n", @src; my $code = eval "sub { $src }"; ## no critic return $code; } sub _croak { my (undef, $file, $line) = caller 1; die @_, " at $file line $line.\n" } sub _register_submeta { my ($class, $pkg, $sub, $types) = @_; my $meta = Sub::Meta->new(sub => $sub, stashname => $pkg); $meta->set_returns(_normalize_types($types)); if (my $materials = Sub::Meta::Finder::FunctionParameters::find_materials($sub)) { $meta->set_is_method($materials->{is_method}); $meta->set_parameters($materials->{parameters}); } Sub::Meta::Library->register($sub, $meta); return; } sub _register_submeta_and_install { my ($class, $pkg, $sub, $types) = @_; my $original_meta = Sub::Meta->new(sub => $sub); my $wrapped = $class->wrap_sub($sub, $types); my $meta = Sub::Meta->new(sub => $wrapped, stashname => $pkg); $meta->set_returns(_normalize_types($types)); if (my $materials = Sub::Meta::Finder::FunctionParameters::find_materials($sub)) { $meta->set_is_method($materials->{is_method}); $meta->set_parameters($materials->{parameters}); } $meta->apply_meta($original_meta); Sub::Meta::Library->register($wrapped, $meta); { no strict qw(refs); no warnings qw(redefine); *{$meta->fullname} = $wrapped; } return; } sub _normalize_types { my $types = shift; if (@$types == 1) { return $types->[0]; } else { return $types; } } 1; __END__ =encoding utf-8 =head1 NAME Function::Return - specify a function return type =head1 SYNOPSIS use Function::Return; use Types::Standard -types; sub foo :Return(Int) { 123 } sub bar :Return(Int) { 3.14 } foo(); # 123 bar(); # ERROR! Invalid type # multi return values sub baz :Return(Num, Str) { 3.14, 'message' } my ($pi, $msg) = baz(); my $count = baz(); # ERROR! Required list context. # empty return sub boo :Return() { return; } boo(); =head1 DESCRIPTION Function::Return allows you to specify a return type for your functions. =head2 SUPPORT This module supports all perl versions starting from v5.14. =head2 IMPORT OPTIONS =head3 no_check You can switch off type check. If you change globally, use C<<$ENV{FUNCTION_RETURN_NO_CHECK}>>: BEGIN { $ENV{FUNCTION_RETURN_NO_CHECK} = 1; } use Function::Return; sub foo :Return(Int) { 3.14 } foo(); # NO ERROR! And If you want to switch by a package, it is better to use the no_check option: use Function::Return no_check => 1; sub foo :Return(Int) { 3.14 } foo(); # NO ERROR! =head3 pkg Function::Return automatically exports a return type by caller. Or you can specify a package name: use Function::Return pkg => 'MyClass'; =head2 ATTRIBUTES =head3 Return C<:Return> attribute is available. =head2 FUNCTIONS =head3 meta This function lets you introspect return values: use Function::Return; use Types::Standard -types; sub baz() :Return(Str) { 'hello' } my $meta = Function::Return::meta \&baz; # Sub::Meta $meta->returns->list; # [Str] In addition, it can be used with L: use Function::Parameters; use Function::Return; use Types::Standard -types; fun hello(Str $msg) :Return(Str) { 'hello' . $msg } my $meta = Function::Return::meta \&hello; # Sub::Meta $meta->returns->list; # [Str] $meta->args->[0]->type; # Str $meta->args->[0]->name; # $msg # Note Function::Parameters::info \&hello; # undef This makes it possible to know both type information of function arguments and return value at compile time, making it easier to use for testing etc. =head2 METHODS =head3 wrap_sub($coderef) This interface is for power-user. Rather than using the C<< :Return >> attribute, it's possible to wrap a coderef like this: my $wrapped = Function::Return->wrap_sub($orig, [Str]); $wrapped->(); =head1 NOTE =head2 enforce LIST to simplify C makes the original function is called in list context whether the wrapped function is called in list, scalar, void context: sub foo :Return(Str) { wantarray ? 'LIST!!' : 'NON!!' } my $a = foo(); # => LIST!! The specified type checks against the value the original function was called in the list context. C is convenient, but it sometimes causes confusion. So, in this module, we prioritize that it easy to understand the type of function return value. =head2 requirements of type constraint The requirements of type constraint of C is the same as for L. Specific requirements are as follows: > The only requirement is that the returned value (here referred to as $tc, for "type constraint") is an object that provides $tc->check($value) and $tc->get_message($value) methods. check is called to determine whether a particular value is valid; it should return a true or false value. get_message is called on values that fail the check test; it should return a string that describes the error. =head2 compare Return::Type Both L and C perform type checking on function return value, but have some differences. 1. C is not possible to specify different type constraints for scalar and list context, but C is possible. 2. C check type constraint for void context, but C doesn't. 3. C can be used together with C, but C seems a bit difficult. =head1 SEE ALSO L =head1 LICENSE Copyright (C) kfly8. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 AUTHOR kfly8 Ekfly@cpan.orgE =cut