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
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 …
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 …
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
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
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 ()
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)
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 …
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)
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;
You have in fact written:
# Raku
my Any $foo = 42;
And the same goes for a parameter to a subroutine:
# Raku
sub foo ($bar) { ... }
Which is in fact:
# Raku
sub foo (Any $bar) { ... }
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
This is not much different in Raku:
# Raku
my $foo;
say defined($foo) ?? "defined" !! "NOT defined";
# NOT defined
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
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)
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
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
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) { ... }
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'?
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
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'?
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)