Skip to content

Methods

Ovid edited this page Sep 14, 2021 · 8 revisions

Please see the main page of the repo for the actual RFC. As it states there:

Anything in the Wiki should be considered "rough drafts."

Click here to leave feedback

As with all of Cor, this is a work in progress and not set in stone.

Method declaration

In Cor, a method is declared using the method keyword. Internally—typeglob slots?—we mark methods as being methods and not subroutines. This ensures that if we call $self->$method, we get a method and not a subroutine we've imported into our namespace.

Further, roles will no longer need to employ heuristics to determine what methods they provide: only things declared with method will be marked as methods, so subroutines will never be exported. Consideration will be needed for consumption of non-Corinna roles.

The structure of a method will be (faking a grammar):

METHOD     ::= MODIFIERS 'method' SIGNATURE '{' (perl code) '}'
SIGNATURE  ::= METHODNAME '(' current sub argument structure + extra work from Dave Mitchell ')'
METHODNAME ::= [a-zA-Z_]\w*
MODIFIERS  ::= MODIFIER { MODIFIER }
MODIFIER   ::= 'has' | 'private' | 'overrides' | 'multi' | 'abstract' | 'common'

This means that more than one modifier might be present:

common private method next_id () { ... }

Instance Methods

class Some::Class {
    method foo() {
        $self->do_something;
        ...
    }
}

Instance methods belong to instances of classes. Thus, calling Some::Class->foo would be an error. It might even be possible to trap some cases of that at compile time.

Class Methods

See also Class Data and Methods.

class Some::Class {
    shared method foo() {
        $class->do_something_else;
        ...
    }
}

Class methods are methods which are shared across all instances of a class. You may call a class method on an instance or a class name. Though constructors in Perl are considering class methods (as Perl does not have a new keyword), we have other cases where we need class methods called. For example, factory classes often have a constructor with a different name to make it clear that you're not instantiating a normal class:

my $connection = Factory::Connection->create($type);

Of course, various helper methods for building an object, prior to object instantion, are also class methods.

There has been an objection to using shared because the :shared attribute is used for sharing data structures between threads:

my $locked :shared;

Given that we also need class data, this is problematic, though we'd use the shared as a declarator and not an attribute:

shared $pi = 3.1415927;

Other suggestions welcome. class was considered, but then we have this:

class Universe {
    class $pi = 3.1415927;
    class method big_bang ($theory) { ... }
}

Overloading the meaning of class seems problematic. Further, if we want to extend Corinna with nested classes, this will complicate parsing.

Possibilities:

  • shares (too similar to shared?)
  • common
  • mutual
  • joint
  • collective
  • have

An Unusual Alternative

It's been pointed out that my and our are both possessive pronouns. For a class, has and shared are analogous, but they are a verb and adjective respectively. What if they were also possessive pronouns?

class Client {
    its   $ID;
    their $next_ID;

    its method newest () { $ID == $next_ID - 1  }   # 'its' is optional

    their method update_next_ID ($new_ID) {
        $next_ID = $new_ID if $new_ID >= $next_ID;
    }
}

Private Methods

Private methods are prefixed with private:

private method do_not_touch () { ... }

Private methods may only be called inside the class or role they're declared in. If a class consumes a role, neither may access the other's private methods.

Note: for those who want Java-style "protected" methods that aren't available outside the class but may be called in a subclass, we don't anticipate supporting this because it's not really needed, but you can also use a leading underscore to "fake" it.

Overridden Methods

A method overrides a parent method if has the same name and accepts the same number of arguments. We use the override keyword for this:

override method execute_customer ($for_real) {
    ...
}

This instantly tells the person that there's a parent method. The parent method may or may not be called by the child method.

Shared methods may not override instance methods and vice-versa.

Using override where there is no parent method should be a fatal, compile-time error.

Failure to use override when there is a parent method might be a warning. This is open for discussion, but likely fails in the case of the method being supplied via role composition. The rationale is simple: if you inherit from a class and someone violates the contract by taking the parent method away, you'd like to know that immediately.

That being said, it's likely that the use of the override modifier would be completely optional.

Abstract Methods

It's been proposed that we allow abstract classes via abstract. If so, allowing abstract methods with the same keyword seems appropriate:

abstract class Subclass::Me {
    abstract method ($foo);
}

Abstract methods must be subclassed and can't be instantiated directly. Similarly, an abstract method must be overridden in a subclass (directly or via role consumption). Failure to do so should be a compile-time error.

Multi Methods

This is the most controversial part of the proposal: multi methods.

Multiple dispatch via multimethods is typically done using a type system. For Cor, since Perl's type system puts data structures before data types, we instead rely on the number of arguments:

multi method authenticate($token)                  { ...  }
multi method authenticate($user,$pass)             { ...  }
multi method authenticate($resource, $user, $pass) { ...  }

This is not perfect (what if you want authenticate($resource,$token)?), but it has the advantage that many common cases in hand-written dispatch can be skipped. Instead, the language itself handles the dispatching. The alternative tends to be something like this:

method authenticate(@args) {
    if ( 1 == @args ) {
        $self->_token_auth(@args);
    }
    elsif ( 2 == @args ) {
        $self->_user_pass_auth(@args);
    }
    elsif ( 3 == @args ) {
        $self->_resource_auth(@args);
    }
}

There are several issues with that.

  1. What happens if you have no arguments? Oops.
  2. What happens if you have more than 3 arguments? Oops.
  3. Error messages, if they exist, are invariably ad-hoc
  4. You have to write scaffolding code that's easy to get wrong in other ways

The failure to take into account the number of arguments is a very common bug when forced to resort to manual dispatching and the if/else chain can be hard to read (given/when is marginally better).

And you have to write _token_auth, _user_pass_auth, and _resource_auth with separate names, even though they're semantically the same thing (reasonable people disagree with whether or not this is a good thing).

Of course, you can get around some of this by sprinkling your code with more and more publc methods:

method token_auth($token)                     { ... }
method user_pats_auth($user,$pass)            { ... }
method resource_auth($resource, $user, $pass) { ... }

Corinna takes the position you want your contract to be as small as possible.

Examples of Multi Methods/Subroutines

Note: multi-examples are courtesy of Damian Conway.

Here's a quicksort implementation in Perl:

sub quicksort {
    my ( $pivot, @list ) = @_;
    return
        !@_    ? ()
      : !@list ? $pivot
      : (
        quicksort( grep { $_ < $pivot } @list ),
        $pivot,
        quicksort( grep { $_ >= $pivot } @list ),
      );
}

It's a trivial example, but it might not be immediately clear to people what's happening. So let's see it with multi subs:

multi sub quicksort ()               { () }
multi sub quicksort ($single)        { $single }
multi sub quicksort ($pivot, @list)  { quicksort(grep {$_ <  $pivot} @list),
                                       $pivot,
                                       quicksort(grep {$_ >= $pivot} @list)
                                     }

And pure functional Mergesort becomes vastly more comprehensible:

    sub merge ($x, $y) {
        !@$x || !@$y        ? ( @$x, @$y                             )
        : $x->[0] < $y->[0] ? ( $x->[0], merge([$x->@[1..$#$x]], $y) )
        :                     ( $y->[0], merge($x, [$y->@[1..$#$y]]) )
    }

    multi sub mergesort ()         {}
    multi sub mergesort ($single)  { $single }
    multi sub mergesort (@list)    { merge
                                        [ mergesort @list[0..@list/2-1]    ],
                                        [ mergesort @list[@list/2..$#list] ]
                                   }

And if you feel the need to build overloaded pseudo-constructors for classes:

class Iterator {
    has $from = 0;
    has $to   = MAX_INT;
    has $step = 1;
    has $next = $from;

    method next () {
        return if $next > $to;
        (my $curr, $next) = ($next, $next+$step);
        return $curr;
    }

    multi shared method seq($to) {
        $class->new(to=>$to-1)
    }

    multi shared method seq($from, $to) {
        $class->new(from=>$from, to=>$to)
    }

    multi shared method seq($from, $then, $to) {
        $class->new(from=>$from, to=>$to, step=>$then-$from)
    }
}

$iter = Iterator->seq(10);
$iter = Iterator->seq(1 => 10);
$iter = Iterator->seq(1, 3 => 10);