Perl Weekly Challenge 107: List Methods

by Abigail

Challenge

Write a script to list methods of a package/class.

Example

package Calc;

use strict;
use warnings;

sub new { bless {}, shift; }
sub add { }
sub mul { }
sub div { }

1;

Output

BEGIN
mul
div
new
add

Discussion

Well, this looks pretty simple. The package in the example has four methods, and the example output list all fo... Wait! It has five lines of output.

What is that BEGIN doing there? There's no method named BEGIN in Calc. And Calc -> can ("BEGIN") is not going to return a reference to it.

Now, it's true that if you define a subroutine named BEGIN, then that subroutine gets executed as BEGIN time; but that does not mean the package contains a method BEGIN in the absence of such a routine.

Perhaps the make of the challenge has a fundamental different view of what a method is than we have. And we don't understand at all why BEGIN gets this special treatment, but not INIT, CHECK, UNITCHECK, nor END.

It's only not clear what should happen in the presence of an AUTOLOAD method. It can be argued that if a class has an AUTOLOAD method, it has any possible method. We decide to not follow that road, and just not list any methods which would trigger the AUTOLOAD method (except the AUTOLOAD method itself).

A further note, we're going to ignore any lexical subroutines. Lexical subroutines are bound to their lexical scope (and hence, found it the scope's lexical pad), not so much to the package.

We also ignore any anonymous subroutines.

Solution

Perl

This is a Perl specific challenge, so we're only presenting a solution in Perl. We take the class name as argument, use the given class name, and search for methods right after.

This assumes that the package is found in file with the same name (after substituting :: with /, and prepending .pm). This is not required in Perl, but it's quite common. Of course, the module must be found in @INC. Here's the code to get the module from the command line, and load it:

my $module = shift;
eval "use $module; 1" or die "Failed to load $module.pm: $@";

After loading, we're going to inspect the symbol table. The symbol table can be accessed as a hash — with a special name: the package name followed by a double colon. So, the symbol table for the package Calc is the hash %Calc::.

The keys of the symbol tables are the symbols (duh!) found in the package. The symbols are the names of the sub routines, package variables (sans sigils), file handles, globs, formats, and other named tokens in the package. Note that different variables can share the same slot: @foo, $foo, sub foo all use the symbol foo.

The values in the symbol tables are typeglobs with information about the symbols. In particular, this typeglob contains entries (slots) indicating what the symbol is used for: SCALAR, ARRAY, HASH, CODE, IO, FORMAT, GLOB if the symbol is used as a scalar, array, hash, code reference, file or directory handle, format or glob. Multiple slots can be present, if the symbol is used for different things.

The slot which is of interest for our solution is the CODE slot. A method is present if the package, if and only if the slot in the symbol table contains a CODE slot.

The sigil used for a typeglob is *.

In our solution, we first create a reference to the symbol table:

my $symbol_table = do {no strict 'refs'; \%{$module . "::"}};

Then we iterate over the keys of the symbol table, and print out each symbol for which there is a CODE slot in the typeglob:

foreach my $symbol (keys %$symbol_table) {
       say $symbol  if *{$$symbol_table {$symbol}} {CODE};
}

Finally, we print BEGIN, if there is an entry for BEGIN in the symbol table, but the glob does not have a CODE slot. We need this so we pass the given example:

say "BEGIN" if $$symbol_table {BEGIN} && !*{$$symbol_table {BEGIN}} {CODE};

Find the full program on GitHub.


Please leave any comments as a GitHub issue.