Skip to content

Instantly share code, notes, and snippets.

@Kaiepi
Last active September 17, 2022 17:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kaiepi/3859a7a8032899ca5a98db9dd8300929 to your computer and use it in GitHub Desktop.
Save Kaiepi/3859a7a8032899ca5a98db9dd8300929 to your computer and use it in GitHub Desktop.
Finding Structure in the Freeform: The Next Wave of Data::Record Changes

Finding Structure in the Freeform: The Next Wave of Data::Record Changes

Note: assumes knowledge gleanable from Data::Record's README and wiki.

I want to do a revision of record types in Data::Record as the user interacts with them from top to bottom, similarly to what v1.x's annotations-based API did for its internals. This would primarily involve the abandonment of the structural, structured, and unstructured keywords in favour of fixed and freeform, which would apply to all core record types. The notion of structured and unstructured data doesn't fit for this module in my view, which I tried to express poorly by naming unstructured data structural, because "structured" only applies to the strictest available wrap, and "unstructured" only applies to the loosest available coercion; the presence of semi-structured data is too large to be ignored.

The Data::Record::Tuple, Data::Record::List, and Data::Record::Map classes backing <@ @>, [@ @], {@ @} respectively should default to a fixed metamode. The wrap, consume, subsume, and coerce modes would pertain to how the elements of the record's container are processed; the fixed and freeform metamodes pertain to the container itself. A fixed record instance must not mutate the structure of the fields in order to validate a typecheck; a freeform record instance is allowed to mutate given the container when typechecks fail in order to at least meet some definition for its modes. Structural maps already do this.

The structural keyword of a map would map to freeform. Tuples can meet this definition by scanning for "firsts" in the way lists do. On the other hand, lists already meet this definition, but can meet the definition of fixed by never skipping a value to consider in the way tuples won't. Maps meet the freeform definition in allowing values not described by the type to exist. For instance, given:

proto MinPercent($) {*}
multi MinPercent(Numeric:U $x --> True) { }
multi MinPercent(Numeric:D $x) { 0 <= $x <= 50 }

proto MaxPercent($) {*}
multi MaxPercent(Numeric:U $x --> True) { }
multi MaxPercent(Numeric:D $x) { 50 <= $x <= 100 }

This should allow for a representation for a type of a list of variables with (<>) given a tuple of firsts to grep for to exist in record terms:

use Data::Record;

my constant MinMaxPercent = <@ &MinPercent, &MaxPercent @>:name<MinMaxPercent>;

my ($a, $b) := (MinMaxPercent (<>) <1 2 99>):freeform; # (1, 99)

Note that the freeform keyword is applied to the coercion, not the type. It may still be applied to such a type, but this should carry a more nuanced meaning. It would serve as a sticky default adverb for coercions. Though existing operators would override such an adverb, wrap, consume, subsume, and coerce can also be applied in this way if the operators delegate elsewhere based on these. This would allow for STORE to be applicable to record types if such a modal adverb is applied, which is the primary barrier to the module allowing for a fully mutable record type because &infix:<=> doesn't take adverbs. COERCE, which didn't exist when this was first written, poses a similar problem.

Currently, new would be the only method this could delegate to. I want to move four common multis from new to a new CALL-ME method:

multi method new(::?CLASS:_: T $record is raw, Bool:D :wrap($)! where ?*)
multi method new(::?CLASS:_: T $record is raw, Bool:D :consume($)! where ?*)
multi method new(::?CLASS:_: T $record is raw, Bool:D :subsume($)! where ?*)
multi method new(::?CLASS:_: T $record is raw, Bool:D :coerce($)! where ?*)

CALL-ME would handle mutations to a record instance in general. If invoked on a type object, this would create an instance to mutate. This could delegate to existing wrap, consume, subsume, and coerce methods to produce their special iterators to back a core record type, just these would now mutate the record in producing its internal container.

The infix (><), (>>), (<<), and (<>) operators can delegate to this instead of new. In a similar fashion, the <@ @>, [@ @], and {@ @} could delegate to a beget method to eliminate the need to work with the operators altogether and allow for subclassing of Data::Record::Tuple, Data::Record::List, and Data::Record::Map. This could help in a scenario where their container Data::Record::Operators module's precomp time is unacceptable (other modules are seriously an order of magnitude shorter to precomp without syntax muckery). Given these changes, the above example could be written:

use Data::Record::Tuple;

my constant MinMaxPercent = Data::Record::Tuple.beget: (&MinPercent, &MaxPercent), :name<MinMaxPercent>;

my ($a, $b) := MinMaxPercent(<1 2 99>):coerce:freeform;

Or perhaps the type can be written this way with the obscure &infix:<:> operator:

my constant MinMaxPercent = (beget Data::Record::Tuple : (&MinPercent, &MaxPercent), :name<MinMaxPercent>);

Which, when nested, can result in (somewhat...) more readable, Lisp-ish code, e.g.

use Data::Record::List;
use Data::Record::Map;

my constant ItsList = (beget Data::Record::Map : (
    list => (beget Data::Record::List : (
       Cool:D,
    ), :name<TheList>),
), :name<ItsList>);

Though separating this into variables for each record type can allow for fewer parentheses. In either case, this would be an example of tuned usage of Data::Record, which itself would become a fatty import capable of covering the average use case. Its individual components could stand on their own this way.

In order to accommodate a mutable record, besides all this, the methods pertaining to Positional, Associative, Array, and Hash that exist on tuples, lists, and maps need more work, which v1.x API's changes make more feasible, but were left alone before. These were only sometimes OK before depending on the type. Otherwise, I don't want to add any more features to the library at the moment; I originally wanted it to be more minimal than what I wound up with anyhow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment