The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Dios - Declarative Inside-Out Syntax

VERSION

This document describes Dios version 0.002013

SYNOPSIS

use Dios;

# Declare a derived class...
class Identity is Trackable {

    # All instances share these variables...
    shared Num %!allocated_IDs;   # Private and readonly
    shared Num $.prev_ID is rw;   # Public and read/write

    # Declare a function (no invocant)...
    func _allocate_ID() {
        while (1) {
            # Declare a typed lexical variable...
            lex Num $ID = rand;

            return $prev_ID =$ID if !$allocated_IDs{$ID}++;
        }
    }

    # Each instance has its own copy of each of these attributes...
    has Num $.ID     = _allocate_ID();  # Initialized by function call
    has Str $.name //= '<anonymous>';   # Initialized by ctor (with default)

    has Passwd $!passwd;                # Private, initialized by ctor

    # Methods have $self invocants, and can access attributes directly...
    method identify (Str $pwd --> Str) {
        return "$name [$ID]" if $pwd eq $passwd;
    }

    # Destructor (submethods are class-specific, not inheritable)...
    submethod DESTROY {
        say "Bye, $name!";
    }
}

DESCRIPTION

This module provides a set of compile-time keywords that simplify the declaration of encapsulated classes using fieldhashes and the "inside out" technique, as well as subroutines with full parameter specifications.

The encapsulation, constructor/initialization, destructor, and accessor generation behaviours are all autogenerated. Type checking is provided by the Dios::Types module. Parameter list features are similar to those provided by Method::Signature or Kavorka.

As far as possible, the declaration syntax (and semantics) provided by Dios aim to mimic that of Perl 6, except where intrinsic differences between Perl 5 and Perl 6 make that impractical, in which cases the module attempts to provide a replacement syntax (or semantics) that is likely to be unsurprising to experienced Perl 5 programmers.

INTERFACE

Declaring classes

The module provides a class keyword for declaring classes. The class name can be qualified or unqualified:

use Dios;

class Transaction::Source {
    # class definition here
}

class Account {
    # class definition here
}

Specifying inheritance relationships

To specify a base class, add the is keyword after the classname:

class Account::Personal is Account {
    # class definition here
}

You can specify multiple bases classes multiple is keywords:

class Account::Personal is Account is Transaction::Source {
    # class definition here
}

Declaring object attributes

Within a class, attributes (a.k.a. fields or data members) are declared with the has keyword:

class Account {

    has $.name is rw //= '<unnamed>';
    has $.ID   is ro   = gen_unique_ID();
    has $!pwd;
    has @.history;
    has %!signatories;

    # etc.
}

Attribute declaration syntax

The full syntax for an attribute declaration is:

    has  <TYPE> [$@%]  [!.]  <NAME>  [is [rw|ro|req]]  [handles <NAME>]  [//=|=] <EXPR>
         ...... .....  ....  ......   ..............    ..............    ... .  ......
            :     :     :      :            :                 :            :  :     :
Type [opt]..:     :     :      :            :                 :            :  :     :
Sigil.............:     :      :            :                 :            :  :     :
Public/private..........:      :            :                 :            :  :     :
Attribute name.................:            :                 :            :  :     :
Readonly/read-write/required traits [opt]...:                 :            :  :     :
Delegation handlers [opt].....................................:            :  :     :
Default initialized [opt]..................................................:  :     :
Always initialized [opt]......................................................:     :
Initialization value [opt]..........................................................:

Typed attributes

Attributes can be given a type, by specifying the typename immediately after the has keyword:

has  Str     $.name;
has  Int     $.ID;
has  PwdObj  $!pwd;
has  Str     @.history;
has  Access  %!signatories;

You can use any type supported by the Dios::Types module. Untyped attributes can store any Perl scalar value (i.e. their type is Any).

As in Perl 6, the type specified for an array or hash attribute applies to each value in the container.

Attribute types are checked on initialization, on direct assignment, and when their write accessor (if any) is called.

Public vs private attributes

An attribute specification can autogenerate read/write or read-only accessor methods (i.e. "getters" and "setters"), if you place a . after the variable's $:

has $.name;    # Generate accessor methods

Such attributes are referred to as being "public".

If you don't want any accessors generated, use a ! instead:

has $!password;    # Doesn't generate accessor methods (i.e. private)

Such attributes are referred to as being "private".

Read-only vs read-write attributes

By default, a public attribute autogenerates only a read-accessor (a "getter" method that returns its current value). To request that full read-write accessors ("getter" and "setter") be generated, specify is rw after the attribute name:

has $.name;          # Autogenerates only getter method
has $.addr is rw;    # Autogenerates both getter and setter methods

You can also indicate explicitly that you only want a getter:

has $.name is ro;    # Autogenerates only getter method

Delegation attributes

You can specify that an attribute is a handler for specific methods, using the handles trait, which must come after any is traits.

To specify that an attribute handles a single method:

has $.timestamp is ro handles date;

Now, any call to ->date on the surrounding object will be converted to a call to $timestamp->date

To specify that an attribute handles a single method, but dispatches it under a different name:

has $.timestamp is ro handles :get_date<date>;

Now, any call to ->get_date on the surrounding object will be converted to a call to $timestamp->date

To specify that an attribute will handle any method of a single class:

has $.timestamp is ro handles Date::Stamp;

Now, any call on the surrounding object to any method provided by the class Date::Stamp will be converted to a call to the same method on $timestamp. For example, if Date::Stamp provides methods date, time, and raw, then any call to any of those methods on the surrounding object will be passed directly to the obkect in $timestamp.

An attribute may specify as many handles traits as it needs.

Get/set vs unified vs lvalue accessors

The accessor generator can build different styles of accessors (just as Object::Insideout can).

By default, accessors are generated in the "STD" style:

has $.name is ro;    # print $obj->get_name();
has $.addr is rw;    # print $obj->get_addr(); $obj->set_addr($new_addr);

However, if the module is loaded with a named "accessor" argument, all subsequent attribute definitions in the current lexical scope are generated with the specified style.

For example, to request a single getter/setter accessor:

use Dios {accessors => 'unified'};

has $.name is ro;    # print $obj->name();
has $.addr is rw;    # print $obj->addr(); $obj->addr($new_addr);

or to request a single lvalue accessor:

use Dios {accessors => 'lvalue'};

has $.name is ro;    # print $obj->name();
has $.addr is rw;    # print $obj->addr(); $obj->addr = $new_addr;

If you want to be explicit about using "STD" style accessors, you can also write:

use Dios {accessors => 'standard'};

Required attributes

Attributes are initialized using the value of the corresponding named argument passed to their object's constructor.

Normally, this initialization is optional: there is no necessity to provide a named initializer argument for an attribute, and no warning or error if none is provided.

If you want to require that the appropriate named initializer value must be present, add is req or is required after the attribute name:

has $.name is req;   # Must provide a 'name' argument to ctor
has $.addr;          # May provide an 'addr' argument, but not necessary

If an initializer value isn't provided for a named argument, the class's constructor will throw an exception.

Initializing attributes

Attributes are usually initialized from the arguments passed to their object's constructor, but you can also provide a default initialization to be used if no initial value is passed, by specifying a trailing //= assignment:

has $.addr //= '<No known address>';

The expression assigned can be as complex as you wish, and can also refer directly to the object being initialized as $self:

state $AUTOCHECK;

has $.addr //= $AUTOCHECK ? $self->check_addr() : '<No known address>';

Note, however that other attributes cannot be directly referred to in an initialization (as they are not guaranteed to have been defined within the object at that point).

Declaring class attributes

Attributes declared with a has are per-object. That is, every object has its own version of the attribute variable, distinct from every other object's version of that attribute.

However, it is also possible to declare one or more "class attributes", which are shared by every object of the class. This is done by declaring the attribute with the keyword shared instead of has:

class Account {

    shared $.status;   # All account objects share this $status variable

    has $.name;        # Each account object has its own $name variable

}

Shared attributes have the following declaration syntax:

shared  [<TYPE>]  [$@%]  [!.]  <NAME>  [is [rw|ro]]  [= <EXPR>] ;
        ........  .....  ....  ......  ............  ..........
            :       :      :      :          :            :
Type [opt]..:       :      :      :          :            :
Sigil...............:      :      :          :            :
Public/private.............:      :          :            :
Attribute name....................:          :            :
Readonly/read-write [opt]....................:            :
Initialization [opt]......................................:

That is, they can have most of the same behaviours as per-object has attributes, except that they are never initialized from the constructor arguments, so they can't be marked is required, and any initialization must be via simple assignment (=), not default assignment (//=).

Like has attributes, shared attributes can be declared as scalars, arrays, or hashes. For example:

class Account {

    shared %is_active; # Track active objects...

    submethod BUILD    { $is_active{$self} = 1;    }
    submethod DESTROY  { delete $is_active{$self}; }
}

Declaring typed lexicals

Dios also supports typed lexical variables, not associated with any class or object, using the keyword lex.

Unlike variables declared with a <my>, variables declared with lex may be given a type, which is thereafter enforced on any subsequent assignment. For example:

lex Str        $name;
lex Num        @scores;
lex Array[Int] %rankings;

As with has and shared variables, the type of a lex array or hash constrains the values of that container. So, in the preceding example, the @scores array can only store numbers, and each value in the %rankings hash must be a reference to an array whose values must be integers.

lex variables can be declared in any scope: in methods or subroutines, in the class block itself, or in the general code. In all other respects apart from type-checking, they are identical to my variables.

Declaring methods and subroutines

Dios provides two keywords, method and func, with which you can declare methods and functions. Methods can only be declared inside a Dios class definition, but functions can be declared in any scope.

A second difference is that methods automatically have their invocant unpacked, either implicitly into $self, or explicitly into a defined invocant parameter.

A third difference is that every method in Dios gets direct private access to its attribute variables. That is: you can refer to an attribute from within a method simply by using its name without the . or ! (see the use of direct lookups on %is_active in the Account class example at the end of the previous section).

Both methods and functions may be declared with a parameter list, as described in the subsequent subsections. If no parameter list is specified, it is treated as an empty parameter list (i.e. as declaring that the method or subroutine takes no arguments).

Parameter list syntax

A function parameter list consists of zero or more comma-separated parameter specifications in parentheses, optionally followed by a return type specification:

func NAME ( PARAM, PARAM, PARAM, ... --> RETTYPE ) { BODY }

A method parameter list consists of an optional invocant specification, followed by the same zero or more parameter specifications:

method NAME ( INVOCANT: PARAM, PARAM, PARAM, ... --> RETTYPE ) { BODY }
method NAME (           PARAM, PARAM, PARAM, ... --> RETTYPE ) { BODY }

As a special case, both methods and functions can be specified with a single ( *@_ ) parameter (note: not ( @_ )), in which case methods still unpack their invocant, but otherwise no parameter processing is performed and the arguments remain in @_.

Invocant parameters

By default, methods have their invocant object unpacked into a parameter named $self. If you prefer some other name, you can specify the invocant parameter explicitly, followed by a colon:

method ($invocant: $other, $args, $here) {...}
method (    $this: $other, $args, $here) {...}
method (      $me: $other, $args, $here) {...}

Note that the colon is essential:

method ($this: $that) {...}  # Invocant is $this, plus one arg

method ($this, $that) {...}  # Invocant is $self, plus two args

Like all other kinds of parameters, explicit invocants can be specified with any type supported by Dios::Types. Generally this makes little sense unless that type is the name of the current class, or one of its base classes, in which case it is merely redundant.

However, the mechanism does have one important use: to specify a class-only or object-only method:

# A method callable only on the class itself
method list_active (Class $self:) {...}

# A method callable only on instances of the class
method make_active (Obj $self:) {...}

Positional parameters

A positional parameter specifies that there must be a corresponding single argument in the argument list, which is then assigned to the parameter variable.

Positional parameters may be specified as scalars:

func add_soldier ($name, $rank, $serial_num) {...}

in which case the corresponding argument may be any scalar value:

add_soldier('George', 'General', 123456);

Positional parameters may also be specified as arrays or hashes, in which case the corresponding argument must be a reference of the same kind. The contents of the referenced container are (shallow) copied into the array or hash parameter variable.

For example:

func show_targets (%hash, @targets) {
    for my $target (@targets) {
        for my $key (keys %hash) {
            say "$key: $hash{$key}" if $key ~~ $target;
        }
    }
}

could be called like so:

show_targets( \%records, [qr/mad/, 'bad', \&dangerous] );

Positional parameters are required by default, so passing the wrong number of positional arguments (either too few or too many) normally produces a run-time exception. See "Optional and required parameters" to change that behaviour.

If the parameters are specified with types, the values must be compatible as well. You can mix typed and untyped parameters in the same specification:

func dump_to (IO $fh, $msg, Obj %data, Bool $sort) {
    say {$fh} $msg;
    for my $key ($sort ? sort keys %data : keys %data) {
        say {$fh} "$key => $data{$key}";
    }
}

As in Dios::Types, a type applied to an array or a hash applies to the individual values stored in that container. So, in the previous example, every value in %data must be an object.

Nameless positional parameters

If a positional parameter will not actually be used inside its subroutine or method, but must still be present in the parameter list (e.g. for backwards compability, or as part of a multimethod signature), then the parameter can be specified by just a sigil without a following name.

For example instead of:

func extract_keys( Str $text, Hash $options ) {
    return [ $text =~ m{$KEY_PAT}g ];
}

you can omit the name of the unused options variable:

func extract_keys( Str $text, Hash $ ) {
    return [ $text =~ m{$KEY_PAT}g ];
}

Moreover, if the nameless parameter variable has a type specifier (as in the preceding example), then you can omit the sigil as well:

func extract_keys( Str $text, Hash ) {
    return [ $text =~ m{$KEY_PAT}g ];
}

Note that this implies that an untyped nameless parameter can be specified either by just its sigil, or by just the generic type Any:

func extract_keys( $text, $ ) {
    return [ $text =~ m{$KEY_PAT}g ];
}

func extract_keys( $text, Any ) {
    return [ $text =~ m{$KEY_PAT}g ];
}

Named parameters

You can also specify parameters that locate their corresponding arguments by name, rather than by position...by prefixing the parameter variable with a colon, like so:

func add_soldier (:$name, :$rank, :$serial_num) {...}

In this version, the corresponding arguments must be labelled with the names of the parameters, but may be passed in any order:

add_soldier(serial_num => 123456, name => 'George', rank => 'General');

Each label tells the method or subroutine which parameter the following argument should be assigned to.

You can specify both positional and named parameters in the same signature:

func add_soldier ($serial_num, :$name, :$rank) {...}

and in any order:

func add_soldier (:$name, $serial_num, :$rank) {...}
func add_soldier (:$rank, :$name, $serial_num) {...}

but the positional arguments must be passed to the call first:

add_soldier(123456, rank => 'General', name => 'George');

although the named arguments can still be passed in any order after the final positional.

Named parameters can also have types specified, if you wish, in which case the type comes before the colon:

func add_soldier ($serial_num, Str :$name, Str :$rank) {...}

You can also specify a named parameter whose label is different from its variable name. This is achieved by specifying the label immediately after the colon (with no sigil), and then the variable (with its sigil) inside a pair of parentheses immediately thereafter:

func add_soldier (:$name, :designation($rank), :ID($serial_num)) {...}

This mechanism allows you to use labels that make sense in the call, but variable names that make sense in the body. For example, now the function would be called like so:

add_soldier(ID => 123456, designation => 'General', name => 'George');

Named parameters can be any kind of variable (scalar, array, or hash). As with positional parameters, non-scalar parameters expect a reference of the appropriate kind, whose contents they copy. For example:

func show_targets (:@targets, :from(%hash),) {
    for my $target (@targets) {
        for my $key (keys %hash) {
            say "$key: $hash{$key}" if $key ~~ $target;
        }
    }
}

which would then be called like so:

show_targets( from => \%records, targets => [qr/mad/, 'bad', \&dangerous] );

Note that, unlike positional parameters, named parameters are optional by default (but see "Optional and required parameters" to change that).

Slurpy parameters

Both named and positional parameters are intrinsically "one-to-one": for every parameter, the method or subroutine expects one argument. Even array or hash parameters expect exactly one reference.

But often you need to be able to create methods or functions that take an arbitrary number of arguments. So Dios allows you to specify one extra parameter that is specially marked as being "slurpy", and which therefore collects and stores all remaining arguments in the argument list.

To specify a slurpy parameter, you prefix an array parameter with an asterisk (*), like so:

func dump_all (*@values) {
    for my $value (@values) {
        dump_value($value);
    }
}

# and later...

dump_all(1, 'two', [3..4], 'etc');

Alternatively, you can specify the slurpy parameter as a hash, in which case it the list of arguments is assigned to the hash (and should therefore be a sequence of key/value pairs). For example:

func dump_all (*%values) {
    for my $key (%values) {
        dump_value($values{$key});
    }
}

...which would be called like so:

dump_all(seq=>1, name=>'two', range=>[3..4], etc=>'etc');

and would collect all four labelled arguments as key/value pairs in %value.

Either kind of slurpy parameter can be specified along with other parameters. For example:

func dump_all ($msg, :$sorted, *@values) {
    say $msg;
    for my $value ($sorted ? sort @values : @values) {
        dump_value($value);
    }
}

When called, the positional arguments are assigned to the positional parameters first, then any labeled arguments are assigned to the corresponding named parameters, and finally anything left in the argument list is given to the slurpy parameter:

dump_all('Look at these', sorted=>1,  1, 'two', [3..4], 'etc');
#         \___________/           V    \___________________/
#             $msg             $sorted        @values

Slurpy parameters can be specified with a type specifier, in which case each value that they accumulate must be consistent with that type. For example, if you're doing a numeric sort, you probably want to ensure that all the values being (optionally) sorted are numbers:

func dump_all ($msg, :$sorted, Num *@values) {
    say $msg;
    for my $value ($sorted ? sort {$a<=>$b} @values : @values) {
        dump_value($value);
    }
}

Named slurpy array parameters

Another option for passing labelled arguments to a subroutine is the named slurpy array parameter.

Unlike a named parameter (which collects just a single labelled value from the argument list), or a slurpy hash parameter (which collects every labelled value from the argument list), a named slurpy array parameter collects every value with a given label from the argument list.

Also unlike a regular slurpy parameter, you may specify two or more named slurpy parameters (as well as one regular slurpy, if you wish).

This allows you to pass multiple separate labelled values and have them collected by name:

func process_caprinae ( *:@sheep, *:goat(@goats) ) {
    shear(@sheep);
     milk(@goats);
}

Such a function might be called like this:

process_caprinae(
    sheep => 'shawn',
     goat => 'billy',
    sheep => 'sarah',
    sheep => 'simon',
     goat => 'nanny',
);

In other words, you can use named slurpy arrays to partition a sequence of labelled arguments into two or more coherent sets.

Named slurpy array parameters may be given a type, in which case every labelled argument value appended to the parameter array must be compatible with the specified type.

Note that named slurpy parameters can only be declared as arrays, since neither hashes nor scalars make much sense in that context.

Constrained parameters

In addition to specifying any Dios::Types-supported type for any kind of parameter, you can also specify a constraint on the parameter, by adding a where block. For example:

func save (
    $dataset   where { length > 0 },
    $filename  where { /\.\w{3}$/ },
   :$checksum  where { $checksum->valid },
   *@infolist  where { @infolist <= 100 },
) {...}

A where block adds a constraint check to the validation of the variable's type, even if the type is unspecified (i.e. it's the default Any).

The block is treated exactly like the constraint argument to Dios::Types::validate() (see Dios::Types for details).

So in the previous example, any call to save requires that:

  • The value passed to the positional $dataset parameter must be (convertible to) a non-empty string,

  • The value passed to the positional filename parameter must be (convertible to) a string that ends in a dot and three characters,

  • The object passed to the named $checksum parameter must return true when its valid() method is invoked, and

  • The number of trailing arguments collected by the slurpy @infolist parameter must no more than 100.

As the previous example indicates, where blocks can refer to the parameter variable they are checking either by its name or as $_. They can also refer to any other parameter declared before it in the parameter list. For example:

func set_range (Num $min, Num $max where {$min <= $max}) {...}

Literal constraints

As a special case of this general parameter constraint mechanism, if the constraint is to match against a literal string or numeric value or a regular expression, then the constraint can be specified by just the literal value.

Note that specifying constrained parameters via constants and patterns is almost only ever useful for multifuncs and multimethods.

For example:

multi factorial ($n where { $n == 0 }) { 1 }
multi factorial ($n)                   { $n * factorial($n-1) }

could be written as just:

multi factorial (0)  { 1 }
multi factorial ($n) { $n * factorial($n-1) }

Likewise:

multi handle_cmd ($cmd where { $cmd eq 'insert'  }, $data) {...}
multi handle_cmd ($cmd where { $cmd eq 'delete'  }, $data) {...}
multi handle_cmd ($cmd where { $cmd eq 'replace' }, $data) {...}

could be simplified to:

multi handle_cmd ('insert',  $data) {...}
multi handle_cmd ('delete',  $data) {...}
multi handle_cmd ('replace', $data) {...}

And:

multi handle_cmd ($cmd where { $cmd =~  /^(quit|exit)$/i  },  $data) {...}
multi handle_cmd ($cmd where { $cmd =~ m{ optimi[zs]e }ix }, $data) {...}

could be specified as just:

multi handle_cmd (  /^(quit|exit)$/i,  $data) {...}
multi handle_cmd ( m{ optimi[zs]e }ix, $data) {...}

At present literal parameters can only be numbers, single-quoted strings, or regexes without variable interpolations.

Optional and required parameters

By default, all positional parameters declared in a parameter list are "required". That is, an argument must be passed for each declared positional parameter.

All other kinds of parameter (named, or slurpy, or named slurpy) are optional by default. That is, an argument may be passed for them, but the call will still proceed if one isn't.

You may also specify optional positional parameters, by declaring them with a ? immediately after the variable name. For example:

func add_soldier ($serial_num, $name, $rank?, $unit?) {...}

Now the function can take either two, three, or four arguments, with the first two always being assigned to $serial_num and $name. If a third argument is passed, it is assigned to $rank. If a fourth argument is given, it's assigned to $unit.

You can also specify any other kind of (usually optional) parameter as being required, by appending a ! to its variable name. For example:

func dump_all ($msg, :$sorted!, *@values!) {...}

Now, in addition to the positional $msg parameter being required, a labelled argument must also be provided for the named $sorted parameter, and there must also be at least one argument for the slurpy @values parameter to be assigned as well.

The ? and ! modifiers can be applied to any parameter, even if the modifier doesn't change the parameter's usual "required-ness". For example:

func add_soldier ($serial_num!, $name!, :$rank?, :$unit?) {...}

Typed and constrained optional parameters

If no argument is passed for an optional parameter, then the parameter will retain its uninitialized value (i.e. undef for scalars, empty for arrays and hashes).

If the parameter has a type or a where constraint, then that type or constraint is still applied to the parameter, and may not be satisfied by the uninitialized value. For example:

func dump_data(
    Int  $offset?,
    Str :$msg,
    Any *@data where { @data > 2 }
) {...}

# and later...

dump_data();
# Error: Value (undef) for positional parameter $offset is not of type Int

dump_data(1..10);
# Error: Value (undef) for named parameter :$msg is not of type Str

dump_data(-1, msg=>'results:');
# Error: Value ([]) for slurpy parameter @data
#        did not satisfy the constraint: { @data > 2 }

The solution is either to ensure the type or constraint can accept the uninitialized value as well:

func dump_data(
    Int|Undef  $offset,
    Str|Undef :$msg,
    Any       *@data where { !@data || @data > 2 }
) {...}

or else to give the optional parameter a type-compatible default value.

Optional parameters with default values

You can specify a value that an optional parameter should be initialized to, if no argument is passed for it. Or if the argument passed for it is undefined. Or false.

To provide a default value if an argument is missing (i.e. not passed in at all), append an = followed by an expression that generates the desired default value. For example:

func dump_data(
    Int $offset                  = 0,
    Str :$msg                    = get_std_msg_for($offset),
    Any *@data where {@data > 0} = ('no', $data)
) {...}

Note that this solves the type-checking problem for optional parameters that was described in the previous section, but only if the default values themselves are type-compatible.

Care must be taken when specifying both optional positional and named parameters. If dump_data() had been called like so:

dump_data( msg=>'no results' );

then the positional parameter would attempt to bind to the first argument (i.e. the label string 'msg'), which would cause the entire call to fail because that value isn't an Int.

Even worse, if the positional parameter hadn't been typed, then the 'msg' label would successfully be assigned to it, so there would be no labelled argument to bind to the named parameter, and the left-over 'no results' string would be slurped up by @data.

The expression generating the default value must be final component of the parameter specification, and may be any expression that is valid at that point in the code. As the previous example illustrates, the default expression may refer to parameters declared earlier in the parameter list.

The usual Perl precedence rules apply to the default expression. That's why, in the previous example, the default values for the slurpy @data parameter are specified in parentheses. If they had been specified without the parens:

Any *@data where {@data > 0} = 'no', $data

then Dios would interpret the , $data as a fourth parameter declaration.

A default specified with a leading = is applied only when no corresponding argument appears in the argument list, but you can also specify a default that is applied when there is an argument but it's undef, by using //= instead of =. For example:

func dump_data(
    Int $offset                  //= 0,
    Str :$msg                    //= get_std_msg_for($offset),
    Any *@data where {@data > 0}   = ('no', $data)
) {...}

With the earlier versions of dump_data(), a call like:

dump_data(undef);

would have failed...because although we are passing a value for the positional $offset parameter, that value isn't accepted by the parameter's type.

But with the $offset parameter's default now specified via a //=, the default is applied either when the argument is missing, or when it's provided but is undefined.

Similarly, you can specify a default that is applied when the corresponding argument is false, using ||= instead of //= or =. For example:

func save_data(@data, :$verified ||= reverify(@data)) {...}

Now, if the labelled argument for $verify is not passed, or if it is passed, but is false, the reverify() function is automatically called. Alternatively, you could use the same mechanism to immediately short-circuit the call if unverified data is passed in:

func save_data(@data, :$verified ||= return 'failed') {...}

Defaulting to $_

The parameter default mechanism also allows you to define functions or methods whose argument defaults to $_ (like many of Perl's own builtins).

For example, you might wish to create an function analogous to lc() and uc(), but which randomly uppercases and lowercases its argument (a.k.a. "HoSTagE-cAsE")

func hc ($str = $_) {
    join  "",
    map   { rand > 0.5 ? uc : lc }
    split //, $str;
}

# and later...

# Pass an explicit string to be hostage-cased
say hc('Send $1M in small, non-sequential bills!');

# Hostage-case each successive value in $_
say hc for @instructions;

Aliased parameters

All the kinds of parameters discussed so far bind to an argument by copying it. That's a safe default, but occasionally you want to pass in variables as arguments, and be able to change them within a function or method.

So Dios allows parameters to be specified with aliasing semantics instead of copy semantics...by adding an is alias modifier to their declaration.

For example:

func double_chomp ($str is alias = $_) {
    $str =~ s{^\s+}{};
    $str =~ s{\s+$}{};
}

func remove_targets (%hash is alias, *@targets) {
    for my $target (@targets) {
        for my $key (keys %hash) {
            delete $hash{$key} if $key ~~ $target;
        }
    }
}

which would then be called like so:

# Modify $input
double_chomp($input);

# Modify %records
remove_targets( \%records, qr/mad/, 'bad', \&dangerous );

You can also specify that a named parameter or a slurpy parameter or a named slurpy parameter should alias its corresponding argument(s).

Note that, under Perl versions earlier than 5.022, aliased parameters require the Data::Alias module.

Read-only parameters

You can also specify that a parameter should be readonly within the body of the subroutine or method, by appending is ro to its definition.

For example:

func link (:$from is ro, :$to is alias) {...}

In this example, the $from parameter cannot be modified within the subroutine body, whereas modifications to the $to parameter are allowed and will propagate back to the argument to which it was bound.

Currently, a parameter cannot be specified as both is ro and is alias. In the future, is ro may actually imply is alias, if that proves to be a performance optimization.

Note the differences between:

is ro

The parameter is a read-only copy

is alias

The parameter is a read-write original

Neither modifier

The parameter is a read-write copy

Note that readonly parameters under all versions of Perl currently require the Const::Fast module.

Return types

Both functions and methods can be specified with a return type at the end of their parameter list, preceded by a "long arrow" (-->). That return type can also have a where constraint. For example:

# Must return an integer...
func fibonacci(Int $n --> Int) {...}

# Must return a string that's a valid ID...
func get_next_ID ( --> Str where {valid_ID($_)} ) {...}

# Must return a list of Account objects...
method find_accounts(Code $matcher --> List[Accounts]) {...}

# Don't return anything (must be called in void context)...
method exit(-->Void) {...}

Functions and methods with specified return types check that any value they return is compatible with their specified type, and throw an exception if the return value isn't.

The special return type Void requires that the function or method returns only undef or an empty list, and that the function be called only in void context. An exception is thrown if the return value is defined; a warning is issued if the call is not in void context.

Any return type apart from Void requires that the function or method be called in scalar or list context (i.e. that the return value that it so carefully checked is not just thrown away). If such a function or method is called in void context, a warning is issued (unless no warnings 'void' is in effect at the point of the call.

Alternatively, you can allow a function or method with an explicit return type to also be called in void context by adding <|Void> to its return-type specifier. For example, this produces a warning:

func normalize ($text --> Str) {...}

normalize($data);   # Warning: Useless call to normalize() in void context

whereas this version does not:

func normalize ($text --> Str|Void) {...}

normalize($data);   # No warning

To declare a function or method that can return either a scalar value or a list (e.g. according to call context), use a compound type. For example:

method Time::subseconds ( --> List|Str ) {
    wantarray ? ($sec, $usec, $normalized)
              : sprintf('%d.$06d%s', $sec, $usec, $normalized)
}

method Time::HMS ( --> Match[\d\d:\d\d:\d\d]|List[Int] ) {
    wantarray ? ($hour,$min,$sec)
              : sprintf('%02d:%02d:%02d', $hour, $min, $sec)
}

Tail-recursion elimination

Dios provides two keywords that implement pure functional versions of the built-in "magic goto". They can be used to recursively call the current subroutine without causing the stack to grow.

The callwith keyword takes a list of arguments and calls the immediately surrounding subroutine again, passing it the specified arguments. However, this new call does not add another call-frame to the stack; instead the new call replaces the current subroutine call on the stack. So, for example:

func recursively_process_list($head, *@tail) {
    process($head);

    if (@tail) {
        callwith @tail;   # Same as: @_ = @tail; goto &recursively_process_list;
    }
}

The callsame keyword takes no arguments, and instead calls the immediately surrounding subroutine again, passing it the current value of @_. As with callwith, the call to callsame does not extend the stack; instead, it once again replaces the current stack-frame. For example:

sub recursively_process_list {
    process(shift @_);

    if (@_) {
        callsame;         # Same as: goto &recursively_process_list;
    }
}

Note that there is currently a syntactic restriction on callwith (but not on callsame). Specifically, callwith cannot be invoked with a postfix qualifier. That is, none of these are allowed:

callwith @args      if @args;
callwith @args      unless $done;
callwith get_next() while active();
callwith get_next() until finished();
callwith $_         for readline();

When you need to invoke callwidth conditionally, use the block forms of the various control structures:

if (@args)         { callwith @args      }
unless ($done)     { callwith @args      }
while (active())   { callwith get_next() }
until (finished()) { callwith get_next() }
for (readline())   { callwith $_;        }

Declaring multifuncs and multimethods

Dios supports multiply dispatched functions and methods, which can be declared using the multi keyword.

Multiple dispatch is where two or more variants of a given function or method are defined, all of which have the same name, but each of which has a unique parameter signature. When such a function or method is called, Dios examines the arguments it was passed and determines the most appropriate variant to invoke.

The rules for selecting the most appropriate variant as the same as in Perl 6, namely:

  1. Eliminate every variant number or types of parameters does not match the number and types of the argument list.

  2. Sort the remaining viable variants according to how constrained their parameter lists are. If two variants have equally constrained parameter lists (or parameter lists for which there is no clear ordering of constrainedness), sort them in order of declaration.

  3. Call the first variant in the sorted list, or throw an exception if the list is empty.

Multifuncs and multimethods are a useful alternative to internal if/elsif cascades. For example, instead of:

func show (Num|Str|Array $x) {
    ref($x) eq 'ARRAY'     ?  '['.join(',', map {dump $_} @$a).']'
  : looks_like_number($x)  ?    $x
  :                           "'$x'"
}

you could write:

multi func show (Array $a) { '['.join(',', map {dump $_} @$a).']'}
multi func show (Num   $n) {   $n   }
multi func show (Str   $s) { "'$s'" }

Note that, when declaring a multifunc the func keyword may be omitted (as in Perl 6). So:

multi func show (Num $n) {...}
multi func show (Str $s) {...}
multi func show (Ref $r) {...}

may also be written as just:

multi show (Num $n) {...}
multi show (Str $s) {...}
multi show (Ref $r) {...}

Methods can also be declared multi, in which case the class of the invocant object is also considered when determining the most appropriate variant to call. Multimethods are, of course, inherited, and may be overridden in derived classes.

Declaring submethods

A submethod is a Perl 6 construct: a method that is not inherited, and hence may be called only on objects of the actual class in which it is defined.

Dios provides a submethod keyword to declare such methods. For example:

class Account {
    method trace_to (IO $fh) {
        carp "Can't trace a ", ref($self), " object";
    }
}

class Account::Traceable is Account {
    submethod trace_to (IO $fh) {
        print {$fh} $self->dump();
    }
}

Now any objects in a class in the Account hierarchy will complain if its trace_to() method is called, except objects in class Account::Traceable, where the submethod will be called instead of the inherited method.

Most unusually, if the same method is called on an object of any class that derives from Account::Traceable, the submethod will not be invoked; the base class's method will be invoked instead.

Submethods are most commonly used to specify initializers and destructors in Perl 6...and likewise under Dios in Perl 5.

Declaring an initializer submethod

To specify the equivalent of an Object::Insideout :Init method in Dios, create a submethod with the special name BUILD and zero or more named parameters. Like so:

class Account {

    has $.acct_name;
    has $.balance;

    submethod BUILD (:$name, :$opening_balance) {
        $acct_name = verify($name);
        $balance   = $opening_balance + $::opening_bonus;
    }
}

Note, however, that these named parameters cannot have the same name as any class attribute.

When the class constructor is called, and passed a hashref with labelled arguments, any arguments matching the named parameters of BUILD are passed to that submethod.

When an object of a derived class is constructed, the BUILD methods of all its ancestral classes are called in top-down order, and can use their respective named parameters to extract relevant constructor arguments for their class.

Declaring a destructor submethod

You can create the equivalent of on Object::InsideOut :Destroy method by creating a submethod with the special name DESTROY. Note that this method is name-mangled internally, so it does not clash with the DESTROY() method implicitly provided by Object::InsideOut.

A DESTROY() submethod takes no arguments (except $self) and it is a compile-time error to specify any.

When an object of a derived class is garbage-collected, the DESTROY methods of all its ancestral classes are called in bottom-up order, and can be used to free resources or do other cleanup that the garbage collector cannot manage automatically. For example:

class Tracked::Agent {
    shared %.agents is ro;

    submethod BUILD (:$ID) {
        $agents{$self} = $ID;
    }

    submethod DESTROY () {
        delete $agents{$self};  # Clean up a resource that the
                                # garbage collector can't reach.
    }
}

Anonymous subroutines and methods

Due to limitations in the behaviour of the Keyword::Simple module (which Dios uses to implement its various keywords), it is not currently possible to use the func or method keywords directly to generate an anonymous function or method:

my $criterion = func ($n) { 1 <= $n && $n <= 100 };
# Compilation aborted: 'syntax error, near "= func"'

However, it is possible to work around this limitation, by placing the anonymous declaration in a do block:

my $criterion = do{ func ($n) { 1 <= $n && $n <= 100 } };
# Now compiles and executes as expected

DIAGNOSTICS

Invalid invocant specification: %s in 'use Dios' statement

Methods may be given invocants of a name other than $self. However, the alternative name you specified couldn't be used because it wasn't a valid identifier.

Respecify the invocant name as a simple identfier (one or more letters, numbers, and underscores only, but not starting with a number).

Can't specify invocant (%s) for %s

Explicit invocant parameters can only be declared for methods and submethods. You attempted to declare it for something else (probably a subroutine).

Did you mean it to be a regular parameter instead? In that case, put a comma after it, not a colon.

Can't declare two parameters named %s in specification of %s

Each parameter is a lexical variable in the subroutine, so each must have a unique name. You attempted to declare two parameters of the same name.

Did you misspell one of them?

Can't specify more than one slurpy parameter

Slurpy parameters (by definition) suck up all the remaining arguments in the parameter list. So the second one you declared will never have any argument bound to it.

Did you want a non-slurpy array or hash instead (i.e. without the *)?

Can't specify non-array named slurpy parameter (%s)

Slurpy parameters may be named (in which case they collect all the named arguments of the same name). However, they always collect them as a list, and so the corresponding parameter must be declared as an array.

Convert the named slurpy hash or scalar you declared to an array, or else declare the hash or scalar as non-slurpy (by removing the *).

Can't use an attribute name as a parameter name in submethod BUILD()

A BUILD() submethod cannot specify named parameters whose names are the same as the name of any attribute of the class.

This is because direct attribute initialization supercedes indirect initialization via BUILD(). In other words, the constructor argument is "stolen" by the attribute before BUILD() is invoked, which means that the identically named BUILD() argument has no constructor argument remaining with which is could be initialized.

Moreover, even if the parameter could be initialized correctly, it would hide the attribute of the same name within the body of the submethod, which makes it impossible to directly access that attribute:

has $.start;

submethod BUILD (:$start) {
    $start = max($start, 0);   # This does NOT assign to the attribute
}

To prevent these subtle initialization behaviours from introducing bugs, declaring a named argument with the same name as an existing attribute is flagged as a compile-time error:

has $.start;

submethod BUILD (:$start) {  # Error here because attr name 'start' reused
    $start = max($start, 0);
}

The workaround is simply to use a different name for the BUILD() parameter, passing its corresponding named argument to the constructor by that different name:

has $.start;

submethod BUILD (:$begin) {
    $start = max($begin, 0);
}
Invalid parameter specification: %s in %s declaration

You specified something in a parameter list that Dios did not understand.

Review the parameter syntax to see the permitted parameter constructs.

'is ro' requires the Const::Fast module (which could not be loaded)

Dios uses the Const::Fast module to ensure "read-only" parameters cannot be modified. You specified a "read-only" parameter, but Dios couldn't find or load Const::Fast.

Did you need to install Const::Fast? Otherwise, remove the is ro from the parameter definition.

'is alias' requires the Data::Alias module (which could not be loaded)

Under Perl versions prior to 5.22, Dios uses the Data::Alias module to ensure "alias-only" parameters are aliased to their arguments. You specified a "aliased" parameter, but Dios couldn't find or load Data::Alias.

Did you need to install Data::Alias? Or migrate to Perl 5.22? Otherwise, remove the is alias from the parameter definition and pass the corresponding argument by reference.

submethod DESTROY cannot have a parameter list

You declared a destructor submethod with a parameter list, but destructors aren't called with any arguments.

%s takes no arguments

The method or subroutine you called was declared to take no arguments, but you passed some.

If you want to allow extra arguments, either declare them specifically, or else declare a slurpy array or hash as a catch-all.

Unexpected extra argument(s) in call to %s

Dios does not allow subroutines or methods to be called with additional arguments that cannot be bound to one of their parameters. In this case it encountered extra arguments at the end of the argument list for which there were no suitable parameter mappings.

Did you need to declare a slurpy parameter at the end of the parameter list? Otherwise, make sure you only pass as many arguments as the subroutine or method is defined to take.

No argument (%s => %s) found for required named parameter %s

You called a subroutine or method which was specified with a named argument that was marked as being required, but you did not pass a name=>value pair for it in the argument list.

Either pass the named argument, or remove the original required status (by removing the trailing ! from the named parameter).

No argument found for %s in call to %s

You called a subroutine or method which was specified with a positional parameter that was marked as being required, but you did not pass a value for it in the argument list.

Either pass the positional argument, or remove the original required status (by adding a trailing ? to the positional parameter).

Missing argument for required slurpy parameter %s

You called a subroutine or method which was specified with a slurpy parameter that was marked as being required, but you did not pass a value for it in the argument list.

Either pass the argument, or remove the original required status (by removing the trailing ! on the slurpy parameter).

Argument for %s is not array ref in call to %s

You called a subroutine or method that specifies a pass-by-reference array parameter, but didn't pass it an array reference.

Either pass an array reference, or respecify the array parameter as a slurpy array.

Argument for %s is not hash ref in call to %s

You called a subroutine or method that specifies a pass-by-reference hash parameter, but didn't pass it a hash reference.

Either pass a hash reference, or respecify the hash parameter as a slurpy hash.

Unexpected second value (%s) for named %s parameter in call to %s

Named parameters can only be bound once, to a single value. You passed two or more named arguments with the same name, but only the first could ever be bound.

Did you misspell the name of the second named argument? Otherwise, respecify the named parameter as a slurpy named parameter.

No suitable %s variant found for call to multi %s

The named multifunc or multimethod was called, but none of the variants found for it matched the types and values of the argument list.

Ambiguous call to multi %s. Could invoke any of: %s

The named multifunc or multimethod was called, but none of the variants found for it matched the types and values of the argument list.

You may need to add an extra variant whose parameter list specification more precisely matches the arguments you passed. Alternatively, you may need to coerce those arguments to more precisely match the parameter list of the variant you were attempting to invoke.

Call to %s not in void context

A function or method with a signature of --> Void was called, but not in void context.

Either call the function or method in void context, or add alternatives to the return type specification to also allow for non-void calls.

Return value %s of call to %s is not of type Void

A function or method with a signature of --> Void was called, but returned a defined value that was thrown away.

Either change the return type specification to allow other values to be returned, or add a return; to ensure the returned value is suitably "void".

Useless call to %s with explicit return type %s in void context

A function or method with a signature that requires an actual return value was called in void context.

To silence this warning, either specify no warnings 'void' before the call, or else modify the specified return type to include Void as an alternative.

Dios uses the Dios::Types module for its type-checking, so it may also generate any of that module's diagnostics.

CONFIGURATION AND ENVIRONMENT

Dios requires no configuration files or environment variables.

DEPENDENCIES

Requires Perl 5.14 or later.

Requires the Keyword::Declare, Sub::Uplevel, Dios::Types, and Data::Dump modules.

If the 'is ro' qualifier is used, also requires the Const::Fast module.

If the 'is alias' qualifier is used under Perl 5.20 or earlier, also requires the Data::Alias module.

INCOMPATIBILITIES

None reported.

BUGS AND LIMITATIONS

Shared array or hash attributes that are public cannot be accessed correctly if the chosen accessor style is 'lvalue', because lvalue subroutines in Perl can only return scalars.

The module relies on the Keyword::Declare module, which means that it will not work at all under Perl 5.20 and that, under Perl 5.14 and 5.16, it specifically needs version 0.3 (NOT version 0.4) of the Keyword::Simple module.

No other bugs have been reported.

Please report any bugs or feature requests to bug-dios@rt.cpan.org, or through the web interface at http://rt.cpan.org.

AUTHOR

Damian Conway <DCONWAY@CPAN.org>

LICENCE AND COPYRIGHT

Copyright (c) 2015, Damian Conway <DCONWAY@CPAN.org>. All rights reserved.

This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See perlartistic.

DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.