When you write a modules, sometimes you'd like to allow your users two ways of operations. A simple one for which you provide a single function and a complex one where they need to instantiate an object of your class and call methods.

Sometimes you'd like to use the same function name for both case.

I found an examples in version 1.000031 of Text::Markdown

Let's see how does it work:

The code snippet

The relevant part of the code was coped here:

examples/method_or_function_in_text_markdown.pm

package Text::Markdown;

sub new {
    my ($class) = @_;
 
    my $self = {  };
    bless $self, $class;
    return $self;
}

sub markdown {
    my ( $self, $text, $options ) = @_;
 
    # Detect functional mode, and create an instance for this run
    unless (ref $self) {
        if ( $self ne __PACKAGE__ ) {
            my $ob = __PACKAGE__->new();
                                # $self is text, $text is options
            return $ob->markdown($self, $text);
        }
        else {
            croak('Calling ' . $self . '->markdown (as a class method) is not supported.');
        }
    }
 
    $options ||= {};
 
    %$self = (%{ $self->{params} }, %$options, params => $self->{params});
 
    return $self->_Markdown($text);
}
 

Using the function / method

It can be used in two ways.

As a function:

use Text::Markdown 'markdown';
my $html = markdown($original_text, { option => "value" });

or as an instance method:

use Text::Markdown;
my $m = Text::Markdown->new;
my $html = $m->markdown($original_text, { option => "value" });

The problem

The big problem that need to be solved is that in the functional mode the first parameter that the markdown function receives is the $original_text while in the OOP case perl will pass the object ($m) in front of the $original_text. So we need a way inside the markdown function to differentiate between the two cases.

In the OOP case we inside the markdown sub:

$self = $m;
$text = $original_text;
$options = { option => "value" };

In the functional case we have inside the markdown sub:

$self = $original_text;
$text = { option => "value" };
$options = undef;

Explanation of the OOP case

While using the function-interface might be simpler, I think it is simple to explain the implementation of the OOP version first.

After loading the module with use Text::Markdown; we call the new method, the constructor of our class. The new method creates a blessed reference to a hash. (In our copy it is an empty hash, in the real code it was filled by some parameters, but that's not the issue for us now.) See the explanation about the constructor in core Perl or the Constructor and accessors in classic Perl OOP or even the Getting started with Classic Perl OOP.

Once we have our instance object in our hand (in the $m variable in the above example) we call the markdown method passing the the $original_text to it that we would like to parse.

In the markdown method we accept 3 parameters. The first is assigned to $self. As explained it the links above, when we call a method with the $object->method(param, param) notation, Perl will take the $object and pass it as the first parameter before the first param. So when we call $m->markdown($original_text, { option => "value" }), perl will actually run markdown($m, $text, { option => "value" }).

The method in the example also accepts a third parameter called $options. It is the second "param" to be passed by the user of the module.

Then there is the code to detect and handle the functional mode. (The comment was in the original code.)

Here we use the ref function to check the reference type of the variable. In the OOP case $self will be a reference to a hash blessed into the Text::Markdown class or to its subclass if there was one. In that case ref $self will return the name of the class. We are not interested in the actual value, just that it has something in it, that it is not empty.

unless (ref $self) { which is the same as if (not ref $self) {.

The condition will enter the block only if $self is not a reference. If we called $m->markdown($original_text, { option => "value" }) then $self is the same as $m for which ref $self returns the name of the class which means we skip the whole block.

In the next line $options ||= {}; we set the options to be by default an empty reference to a hash in case it was not provided.

The code %$self = (%{ $self->{params} }, %$options, params => $self->{params}); takes those options and updates the list of parameters already in the instance. Currently we are not interested in how this works.

Finally we call the _Markdown method that does the real work of parsing the Markdown text.

Explanation of the functional case

In this case we load the module and import the markdown function by use Text::Markdown 'markdown'; then we simply call the markdown function passing the the $markdown_text, and the { option => "value" } to it without creating an object.

In this case the content of $markdown_text will be assigned to the variable $self, the { option => "value"} will be assigned to $text, and the variable $options will be undef. Yes, you are right to be slightly disgusted. The values are assigned to the wrong variables.

This is the price we pay if we want to have the same name work in both ways. This is also what we need to correct in the code so that it will work properly.

When we reach unless (ref $self) { the variable $self will contains plain string for which ref will return false. This means that we enter the block of the unless.

Inside there is a safety check. We check if $self is the same as the name of our package found in the __PACKAGE__ variable. __PACKAGE__ is Text::Markdown in our case. You might wonder why would be pass the name of the package as the first parameter to the markdown function. We probably would not on purpose, but that would happen if we called markdown as a class-method the same way as we called new above.

If the user wrote Text::Markdown->markdown($text);, an easy to make mistake, then $self in the markdown function would contain Text::Markdown. the condition if ( $self ne __PACKAGE__ ) { is there to properly report if that was the case. If the condition fails then we get to the else part that will croak (similar to die, just better).

If the condition is true then we create an instance object using the new method and call the markdown function, but this time as a method. As the comment explains this is the time to fix the misalignment of the parameters. # $self is text, $text is options.

I personally feel the condition should be the other way around, but that's probably just personal taste as I prefer positive code.

Working example

This is a module based on the above code that has all the other requirements I left out from the above code. This is executable:

examples/method_or_function/Module.pm

package Module;
use strict;
use warnings;
use Carp 'croak';

use base 'Exporter';
our @EXPORT_OK = qw(markdown);

sub new {
    my ($class) = @_;
 
    my $self = {  };
    bless $self, $class;
    return $self;
}

sub markdown {
    my ( $self, $text, $options ) = @_;
 
    # Detect functional mode, and create an instance for this run
    unless (ref $self) {
        if ( $self ne __PACKAGE__ ) {
            my $ob = __PACKAGE__->new();
                                # $self is text, $text is options
            return $ob->markdown($self, $text);
        }
        else {
            croak('Calling ' . $self . '->markdown (as a class method) is not supported.');
        }
    }
 
    $options ||= '';
 
    return "Parsing '$text' with options '$options'";
}
 

1;

Here are the tests to show it works:

examples/method_or_function/test.t

use strict;
use warnings;
use Test::More;

plan tests => 5;

use Module 'markdown';

is markdown('text'),             "Parsing 'text' with options ''";
is markdown('other', 'abc'),     "Parsing 'other' with options 'abc'";

my $m = Module->new;
is $m->markdown('hello'),        "Parsing 'hello' with options ''"; ;
is $m->markdown('hello', 'def'), "Parsing 'hello' with options 'def'"; ;

# Testing the incorrect use of the code
eval {
    Module->markdown('try and fail');
};
like $@, qr/^Calling Module->markdown \(as a class method\) is not supported/;

Just as the original code, this one also has one input check to protect against one user error. The calling of markdown as a class-method we discussed above. Probably this was a use-case the author of the module has encountered. (I know I fell in this trap myself several time.)

There are a number of other ways as well to break the code. For example calling markdown({}, 'abc') will return Parsing 'abc' with options ''. We could try to protect our code against this and probably other incorrect uses, but probably it is not worth the effort.