Types: Made for Humans

1513 words | Description of Perl 6's type system and how to define your own subsets.

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.