Skip to content
Ben Davies edited this page Mar 24, 2020 · 16 revisions

What are record types?

Record types are a subkind of classes. These do a delegate role that implements the API for whatever kind of record type this is an instance of, all of which do the Data::Record::Instance role that stubs said API. These have a list of fields, which are provided when the type is created for use in typechecking, and a map of optional named parameters, with which the delegate role is parameterized.

Additionally, there are record template types. These are parametric types which, upon parameterization, call a body block with its type parameters to produce a record type with the fields it returns. This is handled similarly to how role groups handle parameterization.

The HOWs for these two kinds provide metamethods for reflection purposes, but are not documented on this page.

Creating record types

There are three kinds of record types included with this library. These are created using three operators provided:

Tuples

proto sub circumfix<@ @>»(|) {*}

This operator creates record types for tuple-like lists, which may optionally be named. Data::Record::Tuple is the delegate role for these types.

multi sub circumfix<@ @>»(+values, Str:_ :$name --> Mu)
multi sub circumfix<@ @>»(Block:D $block is raw, Str:_ :$name --> Mu)

The first candidate produces a new tuple record type given any number of fields:

my constant IntStrTuple = <@ Int:D, Str:D @> :name('IntStrTuple');
say (42, 'The answer to life, the universe, and everything') ~~ IntStrTuple; # OUTPUT: True

The second candidate produces a new tuple record template type given a body block:

my constant PTuple   = <@{ $_, $_ }@>;
my constant IntTuple = PTuple[Int:D];
say (1, 2) ~~ IntTuple; # OUTPUT: True

Lists

proto sub circumfix:<[@ @]>(|) {*}

This creates record types for lists, which may optionally be named. Data::Record::List is the delegate role for these types.

multi sub circumfix:<[@ @]>(+values, Str:_ :$name --> Mu)
multi sub circumfix:<[@ @]>(Block:D $block is raw, Str:_ :$name --> Mu)

The first candidate produces a list record type given one field. All values in the list must match this field:

my constant IntList = [@ Int:D @] :name('IntList');
say @(1...10) ~~ IntList; # OUTPUT: True

The second candidate produces a list record type given a body block:

my constant PList   = [@{ $_ }@] :name('PList');
my constant StrList = PList[Str:D];
say <foo bar baz> ~~ StrList; # OUTPUT: True

Multi-field list record types for typing flat, repeated lists are planned, but are currently NYI.

Maps

proto sub circumfix:<{@ @}>(|) {*}

This operator creates record types for maps, which may optionally be named. Data::Record::Map is the delegate role for these types. In order for a map to typecheck against a map record type, its pairs must match the record type's fields exactly. Structural typing for maps (where extra fields not in the record type would be allowed) is planned, but is currently NYI.

multi sub circumfix:<{@ @}>(Pair:D $pair is raw, Str:_ :$name, Bool:D :$structural = False --> Mu)
multi sub circumfix:<{@ @}>(+pairs where pairs.all ~~ Pair:D, Str:_ :$name, Bool:D :$structural = False --> Mu)
multi sub circumfix:<{@ @}>(Block:D $block is raw, Str:_ :$name, Bool:D :$structural = False --> Mu)
multi sub circumfix:<{@ @}>(%not-a-block-wtf, Str:_ :$name, Bool:D :$structural = False --> Mu)

The first two candidates create a map record type given any number of pairs as fields:

my constant Contact = {@ name => Str:D, phone_number => Str:D @} :name('Contact');
say { name => 'Ben Davies', contact => '555-8008-1355' } ~~ Contact; # OUTPUT: True

The third candidate creates a map record template type given a body block:

my constant PValue   = {@{ id => Int:D, value => $_ }@};
my constant StrValue = PValue[Str:D];
say { id => 1, value => 'sup lol' } ~~ StrValue; # OUTPUT: True

The fourth candidate dies with X::Data::Record::Block. A hash itself cannot be a field for a map record type, but it's possible for users to accidentally pass one when attempting to pass a block.

Typechecking

As you've probably seen above, typechecking against record types can be done by smartmatching. In signatures, values can be typechecked against record types via a where clause:

my constant StrList = [@ Str:D @] :name('StrList');

proto sub is-str-list(Any --> Bool:D)             {*}
multi sub is-str-list($ where StrList:D --> True) { }
multi sub is-str-list(Any --> False)              { }

say is-str-list <foo bar baz>; # OUTPUT: True
say is-str-list (1, 2, 3);     # OUTPUT: False

While it's useful to be able to typecheck data structures this way, there are two problems with this kind of typecheck:

  • The entire data structure needs to be typechecked every single time. This is very inefficient!
  • This is always done eagerly. That's no good if you want to type a lazy list!

Fortunately, there's another way to handle typechecking with record types that addresses these issues.

Coercions

There are four methods of record type coercion. All of these will typecheck fields in a data structure against the record type's if they exist (dying with X::Data::Record::TypeCheck if this fails), and will recursively coerce record type fields. When a lazy list is coerced to a tuple or list record type, typechecking will be performed lazily (i.e. as the list gets reified). Record instances can for the most part be used in the same way as the original data structure: methods of Positional or Associative (whichever is relevant) are typechecked, and common iterable methods like push and pop are also typechecked.

There are four operators provided for coercions:

proto sub infix:«(><(|) {*}
proto sub infix:«(<<(|) {*}
proto sub infix:«(>>(|) {*}
proto sub infix:«(<>(|) {*}

All of these can be passed a record type and either a data structure or a record instance. When a record instance is passed, it will get retyped to the record type on the other side.

When using these operators, it's important you bind, not assign, to variables! If you don't, you'll lose typechecking for the operations you can perform with record instances. In addition, you cannot use is to type variables as record types because this calls the STORE method after the instance has been created to initialize data structures. This behaviour is simply incompatible with record types.

If you want to restore the original typing of a record instance, this can be done at any time by calling the unrecord method on it.

Wrapping

The data structure is wrapped with the record type, leaving its fields as is. Any missing fields in the data structure will cause this to die with X::Data::Record::Missing, and any extraneous fields will cause this to die with X::Data::Record::Extraneous.

Wrapping can be performed using the (><) operator:

my constant IntList = [@ Int:D @] :name('IntList');

my @one-two-three := (1, 2, 3) (><) IntList;
say @one-two-three; # OUTPUT: (1 2 3)

Consuming

Consuming a data structure will strip it of any extraneous fields, resulting in a data structure with only the fields of the record type. Any missing fields in the data structure will cause this to die with X::Data::Record::Missing.

Consuming can be performed using the (<<) or (>>) operators when the record type is on the blunt end:

my constant Contact = {@ name => Str:D, phone_number => Str:D @};

my %contact-from-json := {
    name         => '1337boi69',
    phone_number => '911',
    constructor  => 'Function'
} (<<) Contact;
say %contact-from-json; # OUTPUT: {name => 1337boi69, phone_number => 911}

Subsuming

Subsuming a data structure will fill out any missing fields, resulting in a data structure with fields matching those of the record type. If a missing field is a definite type (i.e. has a :D smiley), this will die with X::Data::Record::Definite. Any extraneous fields in the data structure will cause this to die with X::Data::Record::Extraneous.

Subsuming can be performed using the (<<) or (>>) operators when the record type is on the sharp end:

my constant IntStrTuple = <@ Int:D, Str:_ @> :name('IntStrTuple');

my @initialized := (7,) (>>) IntStrTuple;
say @initialized; # OUTPUT: (7 (Str))

Coercion

Coercion is a mix of consuming and subsuming; any missing fields will be filled out, and any extraneous fields will be stripped. As with subsuming, missing definite fields will cause this to die with X::Data::Record::Definite. This should never die unless it's impossible for fields in the data structure to typecheck against those of the record type.

Coercion can be performed using the (<>) operator:

my constant Contact = {@ name => Str:_, phone_number => Str:_ @};

my %unrelated := { ayy => 'lmao' } (<>) Contact;
say %unrelated; # OUTPUT: {name => (Str), phone_number => (Str)}
Clone this wiki locally