DEV Community

Cover image for On Subs And Typing
Elizabeth Mattijsen
Elizabeth Mattijsen

Posted on • Updated on

On Subs And Typing

This blog post examines the subtle differences in visibility of subroutines between Perl and Raku and the (gradual) typing core feature of the Raku Programming Language. It assumes you're familiar with signatures — if you're not, read the "Subroutine Signatures" blog post before you continue.

Visibility of subroutines

In Perl, a named subroutine will by default be visible within the scope of the package in which it is defined, regardless of the scope where the definition takes place:

# Perl
{
    sub foo { "bar" }       # visible outside of this scope
}
say foo(); # bar
Enter fullscreen mode Exit fullscreen mode

In Raku, a named subroutine is visible only within the lexical scope in which it is defined:

# Raku
{
    sub foo () { "bar" }     # only visible in this scope
}
say foo();
# ===SORRY!=== Error while compiling …
# Undeclared routine:
#     foo used at line …
Enter fullscreen mode Exit fullscreen mode

Note that "SORRY!" in the Raku error message means that the subroutine foo can't be found at compile time. This is a very useful feature that helps prevent typos in subroutine names when writing invocations of the subroutine.

You could consider subroutine definitions in Raku to always have a my in front, similar to defining lexical variables. Perl also has a (previously experimental) lexical subroutine feature, which has to be specifically activated in versions lower than Perl 5.26:

# Perl 5.18 or higher
no warnings 'experimental::lexical_subs';
use feature 'lexical_subs';
{
    my sub foo { "bar" }   # limit visibility to this scope
}
say foo();
# Undefined subroutine &main::foo called at …
Enter fullscreen mode Exit fullscreen mode

It is possible in both Perl and Raku to prefix the subroutine definition with an our scope indicator, but the result is subtly different.

In Perl, this makes the subroutine visible outside the scope with its short name, but this isn't the case in Raku.

In Raku, lookups of subroutines are always lexical: the use of our on subroutine declarations (regardless of scope) allows the subroutine to be called from outside the namespace in which it is defined, but only if you fully qualify it:

# Raku
module Foo {
    our sub bar () { "baz" }  # make Foo::Bar visible outside
}
say Foo::bar();# baz
Enter fullscreen mode Exit fullscreen mode

This would fail without the our. In Perl, any subroutine can be called from outside the namespace where it is defined:

# Perl
package Foo {
    sub bar { "baz" }     # always visible from the outside
}
say Foo::bar();# baz
Enter fullscreen mode Exit fullscreen mode

In Perl, the names of subroutines that are intended to be "private" (i.e., called from within that scope only and not from outside) usually start with an underscore. But that won't stop them from being called from the outside. In Raku, subroutines that are not intended to be called from the outside are simply invisible.

The our on a subroutine definition in Raku not only indicates that the subroutine can be called fully qualified from the outside: it also indicates that it will be exported if it is part of a module being loaded.

Calling a subroutine

When you call a subroutine in Perl without subroutine signatures enabled, it will call the subroutine if it exists (determined at runtime) and pass the parameters into @_ inside the subroutine. Whatever happens to the parameters inside the subroutine is entirely up to the subroutine (see the blog post about Subroutine Signatures).

When a subroutine is called in Raku, it performs additional checks to see whether the arguments passed to the subroutine match the parameters the subroutine expects before it calls the subroutine's code.

Raku tries to do this as early as possible — if it determines that a call to a subroutine will never succeed, it will tell you at compile time:

# Raku
sub foo () { "bar" }  # subroutine not taking any parameters
say foo(42);          # try calling it with one argument
# ===SORRY!=== Error while compiling …
# Calling foo(Int) will never work with declared signature ()
Enter fullscreen mode Exit fullscreen mode

Note that the error message mentions the type of value (Int) being passed as an argument. In this case, calling the subroutine will fail because the subroutine doesn't accept any argument being passed to it (declared signature ()).

Other signature features

Apart from specifying positional and named parameters in a signature, you can also specify what type these parameters should be. If the parameter type doesn't smartmatch with the argument type, it will be rejected.

In this example, the subroutine expects a single Str argument:

# Raku
# subroutine taking a Str parameter
sub foo (Str $who) { "Hello $who" }
# try calling it with an integer
say foo(42);                        
# ===SORRY!=== Error while compiling …
# Calling foo(Int) will never work with declared signature (Str $who)
Enter fullscreen mode Exit fullscreen mode

It checks both the number of required parameters and the type. Unfortunately, it is not always possible to reliably see this at compilation time.

But there's still the check done at runtime when binding the argument to the parameter:

# Raku
sub foo (Str $who) { "Hello $who" }
# subroutine taking a Str parameter
my $answer = 42;
# try calling it with a variable
say foo($answer);
# Type check failed in binding to parameter ‘$who';
#   expected Str but got Int (42)
#   in sub foo at …
Enter fullscreen mode Exit fullscreen mode

However, if Raku knows the type of variable being passed to the subroutine, it can determine at compile time that the call will never work:

# Raku
# subroutine taking a Str parameter
sub foo (Str $who) { "Hello $who" }
my Int $answer = 42;
# try calling with an Int variable
say foo($answer);
# ===SORRY!=== Error while compiling …
# Calling foo(Int) will never work with declared signature (Str $who)
Enter fullscreen mode Exit fullscreen mode

It should be clear that using typing in your variables and parameters enables Raku to help you find problems quicker!

Gradual typing

The above is usually called "gradual typing". Raku always performs type checks at runtime ("dynamic typing"). But if it can determine at compile time that something will not work, it will tell you so. This is usually called "static typing".

If you're coming from Perl and have experience with Moose (and specifically MooseX::Types), you may worry about the performance implications of adding type information to your code. This is not a concern in Raku, as type checks always occur in Raku with every assignment to a variable or binding of a parameter. That is because if you do not specify a type, Raku will implicitly assume the Any type, which smartmatches with (almost) everything in Raku.

So, if you're writing:

# Raku
my $foo = 42;
Enter fullscreen mode Exit fullscreen mode

You have in fact written:

# Raku
my Any $foo = 42;
Enter fullscreen mode Exit fullscreen mode

And the same goes for a parameter to a subroutine:

# Raku
sub foo ($bar) { ... }
Enter fullscreen mode Exit fullscreen mode

Which is in fact:

# Raku
sub foo (Any $bar) { ... }
Enter fullscreen mode Exit fullscreen mode

Adding type information not only helps find errors in your program, it also allows the optimiser to make better-informed decisions about what it can optimise and how to best optimise it.

Defined or not

If you specify a variable in Perl but don't assign it, it contains the undefined value (aka undef):

# Perl
my $foo;
say defined($foo) ? "defined" : "NOT defined";
# NOT defined
Enter fullscreen mode Exit fullscreen mode

This is not much different in Raku:

# Raku
my $foo;
say defined($foo) ?? "defined" !! "NOT defined";
# NOT defined
Enter fullscreen mode Exit fullscreen mode

So, you can specify which types of values are acceptable in a variable and as a parameter.

But what happens if you don't assign such a variable?

# Raku
my Int $foo;
say defined($foo) ?? "defined" !! "NOT defined";
# NOT defined
Enter fullscreen mode Exit fullscreen mode

The value inside such a variable is still not defined, as in Perl.

However, if you just want to show the contents of such a variable, it is not "undef", as it would be in Perl:

# Raku
my Int $foo;
say $foo;# (Int)
Enter fullscreen mode Exit fullscreen mode

What you see is the string representation of a "type object" in Raku. Unlike Perl, Raku has a multitude of typed "undef" values.

Each class that exists in core, or which you create yourself, is also a type object.

# Raku
class Foo { }
say defined(Foo) ?? "defined" !! "NOT defined";
# NOT defined
Enter fullscreen mode Exit fullscreen mode

If however you instantiate a type object, usually by calling the new method, it becomes an instance. And that qualifies as "defined" as expected:

# Raku
class Foo { }
say defined(Foo.new) ?? "defined" !! "NOT defined";
# defined
Enter fullscreen mode Exit fullscreen mode

Actually, it is a little bit more complicated than that, because a class can define its own defined method and thus mess with this meaning. The .DEFINITE pseudo-method (aka "macro") can be called to really check if something is an instance, or a type object. But usually, calling .defined is enough.

Type smileys

If you specify a constraint on a parameter in a subroutine, you can also indicate whether you want an instance of that type or not:

# Raku
sub foo (Int:D $bar) { ... }
Enter fullscreen mode Exit fullscreen mode

The :D combined with the Int indicates that you want a definite value of the Int type. Because :D is also the emoji for a big smile, this decoration on the type is called a "type smiley".

So what happens if you pass a type object (an undefined value) to such a subroutine?

# Raku
# only accept instances of Int
sub foo (Int:D $bar) { ... }
# call with a type object
foo(Int);
# Parameter '$bar' of routine 'foo' must be an object
# instance of type 'Int', not a type object of type 'Int'.
# Did you forget a '.new'?
Enter fullscreen mode Exit fullscreen mode

Careful readers may realise that this should create a compile-time error. But alas, it doesn't (yet). Although error messages are known to be pretty awesome in Raku, there is still a lot of work to make them even better (and more timely, in this case).

You can also use the :D type smiley on variable definitions to ensure that you provide an initialisation for that variable:

# Raku
my Int:D $foo;  # missing initialisation
# ===SORRY!=== Error while compiling …
# Variable definition of type Int:D
# requires an initializer
Enter fullscreen mode Exit fullscreen mode

Other type smileys are :U (for undefined) and :_ (for don't care, which is the default):

# Raku
# only accept Int type object
sub foo (Int:U $bar) { ... }
# call with an instance of Int
foo(42);
# Parameter '$bar' of routine 'foo' must be
# a type object of type 'Int',
# not an object instance of type 'Int'.
# Did you forget a 'multi'?
Enter fullscreen mode Exit fullscreen mode

Hmmm… what's this multi that seems to be forgotten? Please check the next blog post for more info on that!

Summary

Subroutines in Raku are, by default, visible only in the lexical scope where they are defined. Even prefixing our will not make them visible outside of that lexical scope, but it does allow a subroutine to be called from outside the scope with its full package name (Foo::bar() for a subroutine bar in a package Foo).

Raku allows you to use gradual typing to ensure the validity of arguments to subroutines or assignments to variables. This does not incur any extra runtime costs. Adding typing to your code even allows the compiler to catch errors at compile time, and it allows the optimiser to make better decisions about optimising during runtime.

Top comments (0)