As an addendum, I suppose part of my project design is antiquated and I could simply ditch the ::Interface-provided new_machine() wrapper function entirely, in which case all implementation code would just be in the implementation, like all the other wrappers. So then users just say something like:

    Class::MOP::load_class( $engine_name );
    my $machine = &{$engine_name->can( 'new_machine' )}();

... assuming $engine_name is from a config file, or:

    Class::MOP::load_class( $engine_name );
    my $machine = $engine_name->new();

... rather than what they do now:

    use Muldis::Rosetta::Interface;
    my $machine = Muldis::Rosetta::Interface::new_machine({
        'engine_name' => $engine_name });

... and so the ::Interface become nothing but "requires foo" roles.

But my main point of this email still stands, about addressing seeming issues of Class::MOP's load_class().

-- Darren Duncan

Darren Duncan wrote:
Hello,

So I have some Perl code which has evolved over the years, which already uses Moose, but I'm looking to make it more savvy or modern/enlightened.

And I've also found what appear to be bugs or deficiencies in Class::MOP.

The original code that I'm concerned about updating is a single subroutine that is copied at the end of this email.

It is the sole content of a Perl "module" Muldis::Rosetta::Interface (as seen on CPAN), which is analogous in purpose and design to the Perl DBI module. This non-object function takes a Perl class name as its sole argument, which is analogous to a DBI driver name, loads that class if it isn't already loaded, and creates and returns an object of that class, which is analogous to a DBI driver handle (but that users invoke its methods directly), and does some tests that the purported virtual-machine/driver-handle provides the expected API.

Now I recognize that Class::MOP provides these, and other, utility functions:

  Class::MOP::is_class_loaded($class_name)
  Class::MOP::load_class($class_name)

... and that these ostensibly are better used instead of doing it manually.

However, from my looking through their source, it appears that these methods may not be as robust as I might desire.

Part of my question to you is whether what I'm going to describe next is intentional behavior, or whether it is a candidate for CMOP enhancement. Or whether I've been expecting behavior that I ought not to be.

1. I looked for the implementation of is_class_loaded() and it seems the only implementation is XS, in the file MOP.xs, with no Pure Perl version existing, although the code comments with that XS says there is a Pure Perl version in lib/Class/MOP.pm which one should study to better understand the "grotty logic". Now I interpret this as that the code comment is outdated and the Pure Perl version was removed sometime around when it was decided to make a C compiler a mandatory requirement of using CMOP. If I'm wrong, please point out the Pure Perl version, or else the comment needs updating.

2.  From lib/Class/MOP.pm:

    sub _try_load_one_class {
        my $class = shift;
        return if is_class_loaded($class);
        my $file = _class_to_pmfile($class);
        return do {
            local $@;
            local $SIG{__DIE__};
            eval { require($file) };
            $@;
        };
    }

One thing that I'm doing which CMOP doesn't seem to do is test after an error-free eval that the successfully compiled module actually declares a class of the requested name. For example, by adding another is_class_loaded() call after the "eval" line iff $@ doesn't hold an error. So the way CMOP is now, load_class() seems to return true if it compiles the appropriately named file, and regardless of whether the desired class was declared by it.

That seems to be an error to me.  Would you agree or disagree?

3. I seem to recall reading somewhere, maybe in the Try::Tiny docs, that it is a common error to test $@ for truthiness rather than definedness. CMOP is just testing for truthiness, which would I think fail if someone throws an exception consisting of the number zero or the empty string.

So that also seems to be an error in CMOP.  Agree/disagree?

P.S. Yes, I recognize my own code also has some of the same mistakes of #3, and I'll fix them.

4.  I don't really have any other concerns at the moment about CMOP.

So if load_class() had the semantics I expect, I could then replace half of my subroutine, the part between START SNIP and END SNIP with:

    Class::MOP::load_class( $engine_name )

... wrapped in an eval/try block that enhances the error message.

If any of you have other recommendations for changing my function to further be Moose savvy or otherwise be more modern/enlightened, I welcome them.

P.S. While arguably superfluous, the new_machine() function exists at all because: Like with DBI, the Muldis::Rosetta API has functions/methods to wrap the actual machine/process/value/etc class constructor submethods, so that user code doesn't have to be littered with the names of the actual implementation classes to invoke new() on; all the users need to know is that the classes do ::Interface roles and treat them accordingly.

Thank you. -- Darren Duncan

----------

    sub new_machine {
        my ($args) = @_;
        my ($engine_name) = @{$args}{'engine_name'};

        confess q{new_machine(): Bad :$engine_name arg; it is undefined}
                . q{ or it is the empty string.}
            if !defined $engine_name or $engine_name eq q{};

# START SNIP
# A module may be loaded due to it being embedded in a non-excl file.
        if (!do {
                no strict 'refs';
                defined %{$engine_name . '::'};
            }) {
            # Note: We have to invoke this 'require' in an eval string
            # because we need the bareword semantics, where 'require'
            # will munge the module name into file system paths.
            eval "require $engine_name;";
            if (my $err = $@) {
confess q{new_machine(): Could not load Muldis Rosetta Engine}
                    . qq{ module '$engine_name': $err};
            }
confess qq{new_machine(): Could not load Muldis Rosetta Engine mod} . qq{ '$engine_name': while that file did compile without}
                    . q{ errors, it did not declare the same-named module.}
                if !do {
                    no strict 'refs';
                    defined %{$engine_name . '::'};
                };
        }
# END SNIP
confess qq{new_machine(): The Muldis Rosetta Engine mod '$engine_name'} . q{ does not provide the new_machine() constructor function.}
            if !$engine_name->can( 'new_machine' );
        my $machine = eval {
            &{$engine_name->can( 'new_machine' )}();
        };
        if (my $err = $@) {
confess qq{new_machine(): Th Muldis Rosetta Eng mod '$engine_name'} . qq{ threw an exception during its new_machine() exec: $err};
        }
confess q{new_machine(): The new_machine() constructor function of the} . qq{ Muldis Rosetta Engine mod '$engine_name' did not ret an} . q{ obj of a Muldis::Rosetta::Interface::Machine-doing class.}
            if !blessed $machine or !$machine->isa( 'Moose::Object' )
or !$machine->does( 'Muldis::Rosetta::Interface::Machine' );

        return $machine;
    }


Reply via email to