RFC: Records — a dictionary with a name and a shape (feedback wanted)

Hi all,

Evan floated an idea a while ago that I've now prototyped and would like your opinion on before it goes any further: records — user‑defined record types that generate their own blocks, and are nothing more than dictionaries at runtime.

The problem

Dictionaries are how we build "a thing with named parts" today, and they work — but the key is free text at every use site. "adress" and "address" are both fine in the editor; the mistake only shows up on the phone as a "not found" value, and often much later. The forum has been asking for a way around this for years:

The idea

One new definition block per screen:

define record Person — fields givenName, surname, born (with defaults; a gear to add/remove/rename fields)

Once a record is defined, its blocks appear in the drawer, exactly like a procedure's call block appears once you define the procedure:

  • make Person — one socket per field, defaults filled in
  • Person.surname of … / set Person.born of … to … — field names are dropdowns, not text
  • is … a Person? / as Person … — recognise a record that came back from TinyDB, Web or a list

Rename a field in the definition and every block follows; delete a field and the blocks that use it get a warning. Typeblocking works (Person. lists the fields). The address book from the tutorials, side by side:


dictionary version

record version

Why it's cheap

A record is a dictionary with a "@type" pair naming it — the JSON‑LD convention. Everything the blocks generate uses the dictionary primitives the runtime already has (make-yail-dictionary, yail-dictionary-lookup, yail-dictionary-set-pair, yail-dictionary?), so:

  • no Companion update, no runtime change, Android and iOS alike;
  • every dictionary block still works on a record (it is one), and every list/dictionary tool, TinyDB, Web and JSON round‑trip already handles it;
  • a project that uses records opens fine in any build that has the blocks; the .aia is just blocks.

The only things that live in the editor are the definition, the dropdowns and the warnings — which is exactly where the errors are today.

Where I'd like your thoughts

  1. How many blocks? The prototype has six (define, make, get, set, is a, as). The minimal version is two: define plus a field ▾ of Person ▾ dropdown block that plugs into the existing dictionary blocks — typo‑proof keys and rename for the smallest change, but no make Person with a socket per field. Which would you rather teach?
  2. The name. "record" vs "struct", "object", "type", "shape"… For beginners, is record the right word?
  3. Strictness. By default a getter on a dictionary that lacks the field is a runtime error (not a silent default); the "@type" tag is only checked if you tick strict on the definition. Right default?
  4. Scope. Definitions are per screen, like procedures. Should they be project‑wide?
  5. Anything you'd have wanted this for that the design doesn't cover.

The design doc

The full proposal — problem statement, the blocks, the checks in the editor, the generated YAIL, versioning and the implementation plan — is here, open for comments:

Records in App Inventor — implementation proposal

Please leave your review as comments in the doc (anyone with the link can comment) — that keeps the discussion next to the design it's about — and use this thread for the bigger questions above. The prototype is on a branch (records, off ucr); happy to put up a test build if there's interest.

Thanks — and thanks to Evan for the idea.

Preet

3 Likes

I need to parse this more deeply, but one thing that immediately sticks out to me is the strictness issue. IIRC, Google Protobufs in version 2 had a required/optional flag per field, but in version 3 they did away with that entirely and just made everything optional. I think we may want to consider the same idea since we are already asking the developer to specify defaults, i.e., why make them specify defaults and then enforce strictness--these are opposing issues. For the sake of our users, reducing the chance of an error occurring is best so if they are providing defaults there is no need for strict checking of objects. More to come...

Historically, a record was a symbolic overlay for a contiguous section of external storage, on tape, disk, card,...

We can leave that baggage behind us.

I read the doc.

It is well thought out.

I am loath to ask for anything more, following the old dictum, If You Ask For Too Much, You End Up With Nothing.

1 Like

@ewpatton — agreed, that's the better rule. I've taken strictness out completely.

Now every field simply has a default, so every field is optional: reading a field that isn't there gives the default, as Person fills in whatever a dictionary is missing, and nothing about a record can raise an error any more. The tag is only ever tested by is a Person?, never enforced.

Here's a bit of mischief, I hope I am not shooting myself in the foot ...

What would the implications be of setting the default value of a record field to an anonymous value procedure, to run at Make a Record time?

I would vote for struct over record.

1 Like

I wonder if it would be worth simplifying the design and use a bit by getting rid of the as block and enabling a form of duck typing, so that a dictionary can simply be used as a record if it has a set of keys that conforms to the record’s set of field names.

I might also suggest the name “object” instead of record. It’s consistent with the term JSON, which already appears as a concept in App Inventor and would be semi-familiar to folks who know some JavaScript.

-Mark

Wouldn't that lose the intentionality of a name?

Debits and credits might have the same account and value attributes in a ledger, for example.

I have seen requests for help where the poster presents a set of numbers but has no idea or way of expressing what the numbers mean or how they tie together.

Object beats struct, because of the JSON tie-in.

The duck typing idea might be useful for cases where you want to build an object/struct/record with incomplete information in stages, like buying a tombstone on sale while still alive, leaving completion for a later date.

I missed that block in the doc, maybe because I did not expect a need for coercion.

I apologize for mis-reading your comment.

The Duck Typing analogy is a good idea.

Good question. The record/object setters and getters could raise errors in the case of explicit record/object type mismatches and only use the duck typing when given raw (i.e. untyped) dictionaries. I think that might be a reasonable approach. What do you think?

This sounds like a policy decision in the declaration, similar to storage allocation policy in variable creation/destruction .

On the other hand, for a playground construction set like AI2, it's more instructive to set everything free and let the kids play dress up with the data structures.

@Mark_Friedman
Duck typing is in, in the hybrid shape you described: get and set take any dictionary with no tag and no as required, and the "@type" tag is read by exactly one block, is a, and enforced nowhere. I kept as only because it does one thing duck typing can't — fill in the fields a dictionary lacks with their defaults and tag it, so is a recognizes outside data afterwards. If people would rather have four blocks, as and is a fall together.

On "object": that's the strongest naming argument I've heard, especially since the tag is the JSON-LD "@type". My only hesitation is that it's the one word here a beginner might read as OOP, with methods and inheritance implied.

@ABG

On scope creep — taken to heart: the only thing I added beyond the original six blocks was dropdowns, and everything the feedback suggested removing, I removed.

Defaults can be procedures: the default is a socket, not a text field, so a call block goes right in it and runs when the record is made. One caveat — get currently evaluates the default eagerly, so a side-effecting default would also fire on reads; I'll make that lazy if dynamic defaults are actually wanted.

On intentionality: debits vs. credits is covered. Plug a make Debit into get Credit.amount of and the editor warns — a warning, not an error, and never a refused connection. It only speaks up where the mismatch is knowable and stays quiet otherwise, so staged construction needs no special handling: a half-built dictionary is just a dictionary until you decide it isn't. The kids do get to play dress up — nothing is forbidden, nothing is converted, and the app builds and runs either way.

An optional pre or post creation event block might be a good place to corral cross attribute validation checks or foreign key selection.

On the other hand, a sample app could show how wrapping object creation in a user procedure is good coding practice.

I used TinyDB to play with the idea of procedural defaults.

It reminded me of my doctor's advice.

Me: Doctor, it hurts when I do this (...)
My Doctor: Don't do that.

Defining a procedure as a default is okay, if you want to try and break things.
But don't recommend it.

So, in your new version, does is a require the tag? Hopefully, not :wink:

Also, I would still consider changing the name of as to something like fill defaults. Calling it as might make the user think that that block is necessary in order to use a dictionary as an object and part of my desire for getting rid of as was to make the use of objects simpler and the programs smaller and therefore (hopefully) easier to understand.

I see the appeal, but that really complicates program understanding and analysis if the decision to support duck typing is distributed (among the object type declarations).

FWIW, I don’t know of any language that supports duck typing on a type-by-type basis.