Zydeco » Manual » 03_MethodsSignaturesTypesCoercion

Methods with signatures, type constraints, and coercion

Synopsis

  package MyApp {
    use Zydeco;
    
    class Person {
      has name = "Anonymous";
      
      method greet ( Person $friend ) {
        printf("Hello %s, I am %s.\n", $friend->name, $self->name);
      }
    }
  }
  
  my $alice = MyApp->new_person(name => "Alice");
  my $bob   = MyApp->new_person(name => "Bob");
  
  $alice->greet($bob);  # ==> "Hello Bob, I am Alice."

The main way of interacting with objects is calling their methods. Methods are defined in the object's class, plus any roles that are consumed by the class.

Simple Methods

You can define a method within a class or role using the method keyword.

  package Farming {
    use Zydeco;
    
    class Cow {
      method make_sound {
        say "moo";
      }
    }
  }
  
  MyApp->new_cow->make_sound();     # ==> "moo"

Within method, the arguments passed to the method are available in an array called @_.

  package MyApp {
    use Zydeco;
    
    class Announcer {
      method announce {
        for my $arg ( @_ ) {
          say $arg;
        }
      }
    }
  }
  
  my $ann = MyApp->new_announcer();
  $ann->announce("Hello", "World");

If you run the above code, you'll notice before it announces "Hello", it will announce a line like:

  MyApp::Announcer=HASH(0x55746c744c58)

This is a string representation of the object itself. The first item in the @_ array is the object itself.

The object itself is also available as a variable $self within the method.

  package Farming {
    use Zydeco;
    
    class Cow {
      has noise = "moo";
      method make_sound {
        say $self->noise();
      }
    }
  }
  
  MyApp->new_cow->make_sound();     # ==> "moo"

Another special variable available within methods is $class which contains the object's class as a string. Because of inheritance, this might not be the same class the method was defined in but a subclass.

Required Methods in Roles

A role can indicate that it requires classes that consume it to provide certain methods.

  package Farming {
    use Zydeco;
    
    role Noisy {
      requires noise;
      
      method make_sound {
        say $self->noise();
      }
    }
    
    class Cow with Noisy {
      has noise = "moo";
    }
  }
  
  MyApp->new_cow->make_sound();     # ==> "moo"

If the "Cow" class didn't provide a "noise" method, then "Noisy" would complain about that. (And yes, "Cow" does provide a "noise" method because the "noise" attribute has an accessor method!)

Method Signatures

It is possible to provide a signature for a method; a list of what parameters it expects.

  package Farming {
    use Zydeco;
    use List::Util qw( min );
    
    class Bucket {
      has capacity!;
      has level = 0;
      
      method empty () {
        $self->level( 0 );
        return $self;
      }
      
      method remaining_space () {
        if ($self->level >= $self->capacity) {
          return 0; # already overfull
        }
        
        return $self->capacity - $self->level;
      }
      
      method add ( $given ) {
        my $take = min($given, $self->remaining_space);
        $self->level( $self->level + $take );
        return $given - $take;
      }
    }
  }
  
  # New 10 litre bucket
  my $bucket = MyApp->new_bucket(capacity => 10);
  
  $bucket->add(6);    # Add 6 litres to bucket.
  $bucket->add(6);    # Try to add another 6 litres, but
                      # only takes 4 and returns 2.

The "empty" and "remaining" methods don't take any parameters, so their signature is the empty signature (). If you try to call them with a parameter, they'll throw an error:

  $bucket->empty();                   # ok
  $bucket->empty( "some string" );    # error

An empty signature of () is different from a method with no signature at all. If there is no signature at all, Zydeco will do nothing to check your method's parameters at all, and you are expected to deal with @_ yourself.

The @_ array is still available in methods which have signatures but does not include $self. For methods which have signatures, @_ corresponds to the parameters in the signature.

Optional Parameters

Method parameters are required by default. Missing or unknown parameters result in an error

  $bucket->add(6);     # okay
  $bucket->add();      # throws an error because $given missing
  $bucket->add(3, 3);  # throws an error because extra parameter

Parameters can be made optional by suffixing them with a question mark.

  method add ( $given, $substance? ) {
    my $take = min($given, $self->remaining_space);
    $self->level( $self->level + $take );
    return $given - $take;
  }
  
  $bucket->add(2, "vodka");          # okay
  $bucket->add(2, "schnapps");       # okay
  $bucket->add(5, "orange juice");   # okay
  $bucket->add(1);                   # okay

Currently, required parameters must precede optional parameters, but future releases of Zydeco may also allow required parameters at the end of the signature.

An alternative to making a parameter optional is to provide a default for it.

  method add ( $given, $substance = "Water" ) {
    my $take = min($given, $self->remaining_space);
    $self->level( $self->level + $take );
    return $given - $take;
  }

Slurpy Parameters

Parameters starting with @ or % are slurpy parameters and eat up all the remaining values passed to the method.

  package MyApp {
    use Zydeco;
    
    class Announcer {
      method announce ( $intro, @messages ) {
        say $intro;
        for my $msg ( @messages ) {
          say $msg;
        }
      }
    }
  }
  
  my $intro = "Hello world!";
  my $ann = MyApp->new_announcer();
  $ann->announce($intro, "Hello", "World");

Slurpy parameters must follow any required or optional parameters. Slurpies are always effectively optional in that they may eat up zero values, and cannot have a default.

Named Parameters

Especially if there are more than three or four parameters, positional parameters can get confusing. You can forget which order they come in and what each parameter means. Named parameters can make things more readable.

  method schedule_meeting ( *room, *date, *start_time, *end_time? ) {
    ...;
    
    say "Scheduled meeting at ", $arg->start_time;
  }
  
  $dept->schedule_meeting(
    date        => "2020-02-22",
    start_time  => "15:00",
    end_time    => "16:30",
    room        => "A113",
  );
  
  # or...
  $dept->schedule_meeting({
    date        => "2020-02-22",
    start_time  => "15:00",
    end_time    => "16:30",
    room        => "A113",
  });

Named parameters use an asterisk instead of a dollar sign. For methods using named parameters, a variable $arg is available within the method body. This provides access to all the named parameters.

  $arg->room;           # ==> "A113"
  $arg->date;           # ==> "2020-02-22"
  $arg->start_time;     # ==> "15:00"
  $arg->end_time;       # ==> "16:30"
  $arg->has_end_time;   # ==> true

The $arg variable is an object itself which you get parameter values from by calling methods. It can also be accessed as a hashref though.

Mixed Parameters

It is possible to mix positional and named parameters under certain conditions.

  • Positional arguments must appear at the beginning and/or end of the list. They cannot be surrounded by named arguments.
  • Positional arguments cannot be optional and cannot have a default. They must be required. (Named arguments can be optional and have defaults.)
  • No slurpies!
  method print_html ( $tag, $text, *htmlver = 5, *xml?, $fh ) {
    
    warn "update your HTML" if $arg->htmlver < 5;
    
    if (length $text) {
      print $fh "<tag>$text</tag>";
    }
    elsif ($arg->xml) {
      print $fh "<tag />";
    }
    else {
      print $fh "<tag></tag>";
    }
  }
  
  $obj->print_html('h1', 'Hello World', { xml => true }, \*STDOUT);
  $obj->print_html('h1', 'Hello World',   xml => true  , \*STDOUT);
  $obj->print_html('h1', 'Hello World',                  \*STDOUT);

Placeholder Parameters

A bare sigil can be used as a placeholder for parameters you don't care about.

  method xyz ($foo, $, $bar) {
    say $foo;
    say $bar;
  }
  
  $obj->xyz("x", "y", "z");    # says "x" then "z"

The value is still available in @_ as you would expect.

Placeholders tend to be most useful in method modifiers and multimethods, which are discussed in the following chapters.

Placeholders may have defaults and/or type constraints (see the next section).

Types

Type constraints may be used to ensure that values are of the correct type.

  class Calculator {
    method add ( Num $x, $Num $y ) {
      return $x + $y;
    }
  }

You may use any type from Types::Standard, Types::Common::String, or Types::Common::Numeric, plus any role type or class type that you define via Zydeco.

  package MyApp {
    use Zydeco;
    
    class Person {
      ...;
    }
    
    class Company {
      has employees = [];
      method hire ( Person $employee ) {
        push @{ $self->employees }, $employee;
      }
    }
  }

As type constraints are designed to operate on scalars rather than arrays or hashes, if you need to type check a slurpy parameter, pretend it's a reference.

  method hire ( ArrayRef[Person] @new_employees ) {
    push @{ $self->employees }, @new_employees;
  }

Choosing a Type Name for Your Class

Zydeco usually names types with names that closely correspond to your classes and roles.

  class Foo;            # type name: Foo
  role Bar;             # type name: Bar
  class Foo::Bar;       # type name: Foo_Bar

You can choose a different type name if you prefer:

  class Person {
    type_name Hooman;
  }

If you choose a custom type name, remember to use that, and not the class name, in places where Zydeco expects a type.

  method hire ( Hooman $employee ) {
    ...;
  }

Using Type Constraints for Attributes

As well as in signatures, type constraints can be used in attribute definitions.

  package MyApp {
    use Zydeco;
    
    class Person {
      has name      ( type => Str );
      has children  ( type => 'ArrayRef[Person]' );
    }
  }

Types can optionally be quoted; in the above example this is done because at the point where the attribute definitions are being compiled, the Person type hasn't been defined yet, so cannot be used as a bareword. You can pre-declare it if you like.

  package MyApp {
    use Zydeco declare => ['Person'];
    
    class Person {
      has name      ( type => Str );
      has children  ( type => ArrayRef[Person] );
    }
  }

Defining Custom Types in a Class

Sometimes you need additional type constraints. The easiest way to do that is to define a type library using Type::Library and import it.

  package MyApp {
    use Zydeco;
    use My::Custom::Types -all;
    
    ...;
  }

These types will now be available to all of MyApp's classes and roles.

It is possible to also define a custom type within a single class or role.

  package MyApp {
    use Zydeco declare => ['Person'];
    
    begin {
      my $registry = Type::Registry->for_class($package);
      $registry->add_type( ArrayRef[Person] => 'People' );
    }
    
    class Person {
      has name      ( type => Str );
      has children  ( type => 'People' );
    }
  }

Coercions

Type coercions (also called type conversion, type casting, or type juggling) allows you to automatically convert a value of one type into another.

  package MyApp {
    use Zydeco;
    
    class Person {
      has name = "Anonymous";
      
      method greet ( Person $friend ) {
        printf("Hello %s, I am %s.\n", $friend->name, $self->name);
      }
    }
  }
  
  my $alice = MyApp->new_person(name => "Alice");
  my $bob   = MyApp->new_person(name => "Bob");
  
  $alice->greet($bob);  # ==> "Hello Bob, I am Alice."

The "greet" method expects to be passed a Person object, but what if we wanted to allow it to also accept a string, the person's name?

  package MyApp {
    use Zydeco;
    
    class Person {
      has name = "Anonymous";
      
      coerce from Str via from_string {
        $class->new(name => $_);
      }
      
      method greet ( Person $friend ) {
        printf("Hello %s, I am %s.\n", $friend->name, $self->name);
      }
    }
  }
  
  my $alice = MyApp->new_person(name => "Alice");
  
  $alice->greet("Bob");  # ==> "Hello Bob, I am Alice."

Now the Person class has a "from_string" method, and anywhere the Person type constraint is used, a string will now be accepted and "upgraded" to a Person object via that method.

An alternative technique would be to use a signature that accepted both strings and Person objects.

  package MyApp {
    use Zydeco;
    
    class Person {
      has name = "Anonymous";
      
      method greet ( Person|Str $friend ) {
        my $friend_name = is_Str($friend) ? $friend : $friend->name;
        printf("Hello %s, I am %s.\n", $friend_name, $self->name);
      }
    }
  }
  
  my $alice = MyApp->new_person(name => "Alice");
  
  $alice->greet("Bob");  # ==> "Hello Bob, I am Alice."

The :coercion attribute

If you have an existing method that does something that's essentially a coercion:

  class Person {
    has name;
    
    method from_name ( Str $name ) {
      return $class->new( name => $name );
    }
  }

Then you'd normally make a coercion like this:

  class Person {
    has name;
    
    method from_name ( Str $name ) {
      return $class->new( name => $name );
    }
    
    coerce from Str via from_name;
  }

But a shortcut is:

  class Person {
    has name;
    
    method from_name :coercion ( Str $name ) {
      return $class->new( name => $name );
    }
  }

This only works when the method takes a single, typed, positional argument.

Optimization

Adding :optimize to a method instructs Zydeco to perform additional optimizations at compile-time to improve its run-time speed.

  method foobar :optimize ( $foo, $bar ) {
    ...;
  }

In optimized methods you cannot close over variables.

Abbreviated Syntax

For very short methods, an abbreviated syntax is allowed.

  method is_oversized = $self->value > 100;

The method is defined as usual, but instead of a block, there is an equals sign followed by a scalar expression (i.e. any expression with a higher precedence than comma), followed by a semicolon. The semicolon must be present, even if the method declaration is the last statement in its block.

This has the side-effect of implying :optimize for you, so don't use this syntax if you need to close over a variable.

The abbreviated syntax also works for coercions.

  coerce from Str via from_string = $class->new(name => $_);

Keywords

In this chapter, we looked at the following keywords:

method
requires
type_name
coerce

Todo

This page should probably detail overload.

Next Steps

This chapter covered some very big topics; methods, signatures, and type constraints and coercions. Signatures and types will be used quite a lot in the next three chapters as well.