-
Notifications
You must be signed in to change notification settings - Fork 0
Basics
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 (which is exported from Data::Record
). 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.
There are three kinds of record types included with this library. These are created using three operators provided:
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, which is exported from Data::Record
.
multi sub circumfix:«<@ @>»(+@fields is raw, 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
proto sub circumfix:<[@ @]>(|) {*}
This creates record types for lists, which may optionally be named. Data::Record::List
is the delegate role for these types, which is exported from Data::Record
.
multi sub circumfix:<[@ @]>(+@values is raw, Str:_ :$name --> Mu)
multi sub circumfix:<[@ @]>(Block:D $block is raw, Str:_ :$name --> Mu)
The first candidate produces a list record type given a list of fields. If there is one field, all values in the list must match this field:
my constant IntList = [@ Int:D @] :name('IntList');
say @(1...10) ~~ IntList; # OUTPUT: True
If there is more than one field, the list must have a length that divides by the number of fields, and values in the list must match the field at the value's index in the list modulo the number of fields. In other words, you wind up with a type for flat, repeated lists:
my constant IntStrList = [@ Int:D, Str:D @] :name('IntStrList');
say (1,) ~~ IntStrList; # OUTPUT: False
say (1, 'a') ~~ IntStrList; # OUTPUT: True
say (1, 'a', 2) ~~ IntStrList; # OUTPUT: False
say (1, 'a', 2, 'b') ~~ IntStrList; # OUTPUT: True
For lazy lists, no length checking is performed until the list is fully reified, so list record types can be used to type streams of values. How lazy lists can be typed using record types will be explained later.
The second candidate produces a list record template type given a body block:
my constant PList = [@{ $_ }@] :name('PList');
my constant StrList = PList[Str:D];
say <foo bar baz> ~~ StrList; # OUTPUT: True
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, which is exported from Data::Record
. In order for a map to typecheck against a map record type normally, its pairs must match the record type's fields exactly. However, if the :structural
adverb is passed, a structural map type is created, which allows extraneous pairs to exist in the type.
multi sub circumfix:<{@ @}>(Pair:D $pair is raw, Str:_ :$name, Bool:D :$structural = False --> Mu)
multi sub circumfix:<{@ @}>(+@pairs is raw, Str:_ :$name, Bool:D :$structural = False --> Mu)
multi sub circumfix:<{@ @}>(Block:D $block is raw, 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 => $_ }@} :name('PValue');
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.
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.
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.
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 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 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 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)}