3

We use Perl Moo.

Let there is defined a set of attributes:

package C;
use Moo;
use Types::Standard qw(Str Int Num Maybe);

has 'x' => (is=>'rw', isa=>Str);
has 'y' => (is=>'rw', isa=>Int);
has 'z' => (is=>'rw', isa=>Int);

# here to insert make_optional() described below

1;

I want to write a routine which will replace T with Maybe[T] for some attributes. For example: make_optional(qw(x y)) should make type of x Maybe[Str] and type of y Maybe[Int].

How to do it with Moo?

porton
  • 5,214
  • 11
  • 47
  • 95
  • 2
    As with a lot of the questions you've had, I'm asking myself: **why**? – simbabque Sep 16 '16 at 13:14
  • @simbabque We have some (lightweight and not feature rich) ORM (a level above DBI + SQL). Now I am ordained to make "business objects" (a level above ORM). Some of the types (and `has` statements) for business objects are automatically generated from DB columns information. But I need to manually specify the list of columns which can take `undef` value (not the same as NULL columns) (`Maybe` types) and do it in an easy way. – porton Sep 16 '16 at 13:26
  • Can't you write a code generator and run it whenever the table layout changes like you would with DBIC? – simbabque Sep 16 '16 at 13:31
  • @simbabque We already have such a code generator. The trouble is that there is no one-to-one correspondence between NULL columns and attributes which can take `undef` value. Yes, this is silly; but this is our reality – porton Sep 16 '16 at 13:38
  • 1
    then keep in that code generator a list of columns that are wrong, and fix them in the generated code. or if worst came to worst, have an empty dummy copy of the database structure, and a sql script that makes all the wrong columns nullable, and run your code generator on that database. but try to fix it in the actual generated code, not in something that runs after your code is running. – ysth Sep 16 '16 at 13:50

2 Answers2

5

You can't.

Moo does not have a Meta Object Protocol. Without it, you cannot go back and alter stuff.

There is no meta object. If you need this level of complexity you need Moose - Moo is small because it explicitly does not provide a metaprotocol.

Furthermore, the types are just code refs.

There is no built-in type system. isa is verified with a coderef; if you need complex types, Type::Tiny can provide types, type libraries, and will work seamlessly with both Moo and Moose.


What you could do is maybe write a type that access some kind of singleton somewhere else to decide if it behaves like Maybe[Str] or Str, but that's a long shot and probably ugly and crazy and you shouldn't do it.

simbabque
  • 53,749
  • 8
  • 73
  • 136
  • But it seems that I can get a coderef `c` and replace it with `Maybe[c]`. The question is where to get the type coderef for a given attribute. – porton Sep 16 '16 at 13:33
  • 1
    @porton that could be a new question. Or go and ask in #moose. They'll try to talk you out of it, but they'll help. – simbabque Sep 16 '16 at 16:54
2

[[Note that the Maybe type doesn't really make an attribute optional per-se, but undef-tolerant. Moo attributes are already optional by default. But for the sake of discussion, I'll continue to use the terminology of optional versus required.]]

Because I don't like "you can't" answers, here's some code that does what you want...

use strict;
use warnings;

BEGIN {
    package MooX::MakeOptional;
    use Types::Standard qw( Maybe Any );
    use Exporter::Shiny our @EXPORT = qw( make_optional has );
    use namespace::clean;
    
    sub _exporter_validate_opts {
        my $opts = pop;
        $opts->{orig_has} = do {
            no strict 'refs';
            \&{ $opts->{into} . '::has' };
        };
        $opts->{attributes} = [];
        'namespace::clean'->clean_subroutines( $opts->{into}, 'has' );
    }
    
    sub _generate_has {
        my $opts = pop;
        
        my $attributes = $opts->{attributes};
        
        return sub {
            my ( $name, %spec ) = @_;
            if ( ref($name) eq 'ARRAY' ) {
                push @$attributes, $_, { %spec } for @$name;
            }
            else {
                push @$attributes, $name, \%spec;
            }
        };
    }
    
    sub _generate_make_optional {
        my $opts = pop;
        
        my $attributes = $opts->{attributes};
        my $orig_has   = $opts->{orig_has};
        
        return sub {
            my %optional;
            $optional{$_} = 1 for @_;
            
            while ( @$attributes ) {
                my ( $name, $spec ) = splice( @$attributes, 0, 2 );
                if ( $optional{$name} ) {
                    $spec->{isa} = Maybe[ $spec->{isa} or Any ];
                }
                $orig_has->( $name, %$spec );
            }
        }
    }
}

{
    package C;
    use Moo;
    use MooX::MakeOptional;
    use Types::Standard qw( Str Int );

    has 'x' => ( is => 'rw', isa => Str );
    has 'y' => ( is => 'rw', isa => Int );
    has 'z' => ( is => 'rw', isa => Int );

    make_optional( qw(x y) );
}

What this does is replace Moo's has keyword with a dummy replacement which does nothing except stash the attribute definitions into an array.

Then when make_optional is called, this runs through the array, and passes each attribute definition to Moo's original has keyword, but altered to be optional if specified.

Classes that use MooX::MakeOptional always need to ensure they call the make_optional function at the end of the class definition, even if they have no optional attributes. If they have no optional attributes, they should just call make_optional and pass it an empty list.

tobyink
  • 13,478
  • 1
  • 23
  • 35