Perl 6 Types: Made for Humans
In my first college programming course, I was taught that Pascal language
has Integer
, Boolean
, and String
types among others. I learned the types
were there because computers were stupid. While dabbling in C, I learned more
about what int
, char
, and other vermin looked like inside the warm,
buzzing metal box under my desk.
Perl 5 didn’t have types, and I felt free as a kid on a bike, rushing through the wind, going down a slope. No longer did I have to cram my mind into the narrow slits computer hardware dictated me to. I had data and I could do whatever I wanted with it, as long as I didn’t get the wrong kind of data. And when I did get it, I fell off my bike and skinned my knees.
With Perl 6, you can have the cake and eat it too. You can use types or avoid them. You can have broad types that accept many kinds of values or narrow ones. And you can enjoy the speed of types that represent the mind of the machine, or you can enjoy the precision of your own custom types that represent your mind, the types made for humans.
Gradual Typing
my $a = 'whatever';
my Str $b = 'strings only';
my Str:D $c = 'defined strings only';
my int $d = 16; # native int
sub foo ($x) { $x + 2 }
sub bar (Int:D $x) returns Int { $x + 2 }
Perl 6 has gradual typing, which means you can either use types or avoid them. So why bother with them at all?
First, types restrict the range of values that can be contained in your variable, accepted by your method or sub or returned by them. This functions both as data validation and as a safety net for garbage data generated by incorrect code.
Also, you can get better performance and reduced memory usage when using native, machine-mind types, providing they’re the appropriate tool for your data.
Built-In Types
There’s a veritable smörgåsbord of built-in types in Perl 6. If the thing your subroutine does makes
sense to be done only on integers, use an Int
for your
parameters.
If negatives don’t make sense either, limit the range of values even further
and use a UInt
—an unsigned Int
. On the other hand, if you want to handle
a broader range, Numeric
type may
be more appropriate.
If you want to drive closer to the metal, Perl 6 also offers a range of
native types that map into what you’d normally find with, say, C
. Using these
may offer performance improvements or lower memory usage. The available types
are: int
, int8
, int16
, int32
, int64
, uint
, uint8
, uint16
, uint32
, uint64
, num
, num32
, and num64
. The number in the type name
signifies the available bits, with the numberless types being
platform-dependent.
Sub-byte types such as int1
, int2
, and int4
are planned to be implemented
in the future as well.
Smileys
multi foo (Int:U $x) { 'Y U NO define $x?' }
multi foo (Int:D $x) { "The square of $x is {$x²}" }
my Int $x;
say foo $x;
$x = 42;
say foo $x;
# OUTPUT:
# Y U NO define $x?
# The square of 42 is 1764
Smileys are :U
, :D
, or :_
appended to the type name. The :_
is the
default you get when you don’t specify a smiley. The :U
specifies
undefined values only
, while :D
specifies defined values only
.
This can be useful to detect whether a method is called on the class or on the
instance by having two multies with :U
and :D
on the invocant. And if you
work at a nuclear powerplant, ensuring your rod insertion subroutine never
tries to insert by an undefined amount is also a fine thing, I imagine.
Subsets: Tailor-Made Types
Built-in types are cool and all, but most of the data programmers work with doesn’t match them precisely. That’s where Perl 6 subsets come into play:
subset Prime of Int where *.is-prime;
my Prime $x = 3;
$x = 11; # works
$x = 4; # Fails with type mismatch
Using the subset
keyword, we created a type called Prime
on the fly. It’s
a subset of Int
, so anything that’s non-Int
doesn’t fit the type. We
also specify an additional restriction with the where
keyword; that
restriction being that .is-prime
method called on the given value must
return a true value.
With that single line of code, we created a special type and can use it as if it were built-in! Not only can we use it to specify the type of variables, sub/method parameters and return values, but we can test arbitrary values against it with the smartmatch operator, just as we can with built-in types:
subset Prime of Int where *.is-prime;
say "It's an Int" if 'foo' ~~ Int; # false, it's a Str
say "It's a prime" if 31337 ~~ Prime; # true, it's a prime number
Is your “type” a one-off thing you just want to apply to a single variable?
You don’t need to declare a separate subset
at all! Just use the where
keyword after the variable and you’re good to go:
multi is-a-prime (Int $ where *.is-prime --> 'Yup' ) {}
multi is-a-prime (Any --> 'Nope') {}
say is-a-prime 3; # Yup
say is-a-prime 4; # Nope
say is-a-prime 'foo'; # Nope
The -->
in the signature above is just another way to indicate the return
type, or in this case, a concrete returned value. So we have two multies with different
signatures. First one takes an Int
that is a prime number and the second
one takes everything else. With exactly zero code in the bodies of our multies
we wrote a subroutine that can tell you whether a number is prime!!
Pack it All Up for Reuse
What we’ve learned so far is pretty sweet, but sweet ain’t awesome! You may end up using some of your custom types quite frequently. Working at a company where product numbers can be at most 20 characters, following some format? Perfect! Let’s create a subtype just for that:
subset ProductNumber of Str where { .chars <= 20 and m/^ \d**3 <[-#]>/ };
my ProductNumber $num = '333-FOOBAR';
This is great, but we don’t want to repeat this subset stuff all over the place.
Let’s shove it into a separate module we can use
.
I’ll create /opt/local/Perl6/Company/Types.pm6
because /opt/local/Perl6
is the path included in module search path for all the apps I write for
this fictional company. Inside this file, we’ll have this code:
unit module Company::Types;
my package EXPORT::DEFAULT {
subset ProductNumber of Str where { .chars <= 20 and m/^ \d**3 <[-#]>/ };
}
We name our module and let our shiny subsets be exported by default. What will our code look like now? It’ll look pretty sweet—no, wait, AWESOME—this time:
use Company::Types;
my ProductNumber $num1 = '333-FOOBAR'; # succeeds
my ProductNumber $num2 = 'meow'; # fails
And so, with a single use
statement, we extended Perl 6 to provide
custom-tailored types for us that match perfectly what we want our data to be
like.
Awesome Error Messages for Subsets
If you’ve been actually trying out all these examples, you may have noticed a minor flaw. The error messages you get are Less Than Awesome:
Type check failed in assignment to $num2;
expected Company::Types::EXPORT::DEFAULT::ProductNumber but got Str ("meow")
in block <unit> at test.p6 line 3
When awesome is the goal, you certainly have a way to improve those messages.
Pop open our Company::Types
file again, and extend the where
clause
of our ProductNumber
type to include an awesome error message:
subset ProductNumber of Str where {
.chars <= 20 and m/^ \d**3 <[-#]>/
or warn 'ProductNumber type expects a string at most 20 chars long'
~ ' with the first 4 characters in the format of \d\d\d[-|#]'
};
Now, whenever the thing doesn’t match our type, the message will be included
before the Type check...
message and the stack trace, providing more info on
what sort of stuff was expected. You can also call fail
instead of warn
here, if you wish, in which case the Type check...
message won’t be printed,
giving you more control over the error the user of your code receives.
Conclusion
Perl 6 was made for humans to tell computers what to do, not for computers to restrict what is possible. Using types catches programming errors and does data validation, but you can abstain from using types when you don’t want to or when the type of data you get is uncertain.
You have the freedom to refine the built-in types to represent exactly the data you’re working with and you can create a module for common subsets. Importing such a module lets you write code as if those custom types were part of Perl 6 itself.
The Perl 6 technology lets you create types that are made for Humans. And it’s about time we started telling computers what to do.
UPDATE:
Perl 6 will actually evaluate your where
expression when checking types even for optional parameters. This can be quite annoying, due to “uninitialized” values being compared. I wrote Subset::Helper to make it easier to create subsets that solves that issue, and it provides an easy
way to add awesome error messages too.
But don't fall into the trap of creating a type you can't instantiate:
http://stackoverflow.com/q/34612657/1030675
OK, but can you define operators on custom made types? A 'date' type is nice, but if you can't subtract, add and other operators, what good is it?
Of course! Custom operators are just subs. If you can use a subset in a signature of the sub, same applies to an operator.
Are there any performance gains/hits when using variable types?