CONSTRUCT Dialect for Objects (Maps?)

Catching up with patterns in @rgchris's modern codebases, he began to focus on being more frugal with symbols. You see many MAP! literals, and the embrace of the "less noisy" underscore to indicate empty fields:

I've spoken previously about my bone to pick with literal forms: Rebol's power comes from evaluation. No evaluator means no magic, and I don't want to be going in that direction.

But I've been inspired by the symbolic economy he has been achieving. Once you start to see that you were "spending money" on a colon for a SET-WORD, you start to think that spending it on an underscore could be a better investment.

Now that we've opened our minds a bit, why don't we open it a bit more?... :exploding_head:

What if our object-creation dialect declares a null-initialized field by default for WORD!, and any fields you want to assign use GROUP! after it?

You could even use quasi-WORD! to say "I want to define this as a field, but make it trash by default, not null"

{
    type ('document)
    name
    public
    system
    form
    ~head~
    body (copy [])
    ~parent~
    first
    last
    warnings
}

(No real reason to pick head and parent as being trash with the others not, I'm just pointing out the way such a feature might work.)

Or maybe a WORD! is implicitly a null field definition, and you can use SET-WORD to assign and it's assumed you know what you're doing:

{
    type: 'document
    name
    public
    system
    form
    head: ~
    body: copy []
    parent: ~
    first
    last
    warnings
}

It leaves the choice to parenthesize up to you. If you think you need them, use them. If not, don't.

The syntaxes aren't actually in contention, the same dialect can offer all the choices:

 type ('document)
 -or-
 type: 'document
 -or-
 type: ('document)

 ~parent~
 -or- 
 parent: ~ 
 -or-
 parent (~)

That's fairly liberal, but this is the game we play... use something more rigid if it suits you.

Tune it to your tastes... per person, per project, per file, per function... that's the exciting thing about this, which we don't entirely get across when we debate the fundamentals for so long...

I think what I'm trying to get at here is:

The real power wasn't in the underscore. It was inside YOU (and dialects) all along...

:slight_smile:

And if @rgchris is super-pleased by this kind of dialect concept, it may be enough of a winner that it earns the default {...} behavior.

We might then have {{key1 val1 key2 val2}} make maps, and everyone can be happy.

2 Likes

Furthering this dialect... JavaScript has a shorthand, where if you use just a name, you can reference a variable and proxy its value into the object under that name:

const name = "Alice";
const age = 30;

// Traditional way
const person1 = {
  name: name,
  age: age
};

// Shorthand property names
const person2 = {
  name,
  age
};

console.log(person1); // { name: 'Alice', age: 30 }
console.log(person2); // { name: 'Alice', age: 30 }

Ren-C has parts. Can do this too, why not use @WORD! (or @TUPLE!)?

let name: "Alice"
let age: 30

let person: {
  @name
  @age
}

probe person  ; &[object! [name: "Alice" age: 30]]

(Having the @ symbol for that makes it seem more in proportion to how weird what you're doing is. Plain WORD! for just spec'ing an empty field seems to fit better.)

2 Likes

GROUP! seems pretty natural for this.

But what if you could use BLOCK! to implement elided code, midstream... with . bound to the object being created (so .field would work)...

type: block!

{
    type ('document)
    name
    public
    system
    form
    [assert [unset? $.body, type = block!, .type = 'document]]
    ~head~
    body (copy [])
    ~parent~
    first
    [assert [block? .body]]
    last
    warnings
}

That would give this dialect back some of the missing power of running arbitrary code between field definitions...which I don't think is likely to be needed all that often besides in debug situations, but hey. It could be useful!

Now what would FENCE! inside FENCE! do? :thinking:

I've already suggested that {{...}} might be a different constructor altogether (for making maps, or something?) so that should probably be kept in mind if trying to come up with another semantic which would apply to {{...} {...}} -- it should be something that doesn't conflict with the reduced case, if it's defined.

2 Likes

I'm contemplating merging this syntax with that of locals for functions.

Something I do there is I allow you to declare a local as ^META. It doesn't currently do anything differently, but it helps you sort of track when a variable is being used mostly to pipe unprocessed results around with limited examinations. If you define it as <local> ^result then you're more attuned to thinking that the references should probably be ^result as well.

Hence it's only been a comment. But I got to wondering: what might it mean in the CONSTRUCT dialect?

{
   foo  ; declare field, initialized to null
   ~baz~  ; declare field, initialized to trash
   ^bar   ; what about...declare field, in the (void!) state?
   /foo   ; also declare field as void
}

That may seem arbitrary, but it's not exactly. ^META variables are frequently used to accrue evaluation state, and VOID! is what those states start out as.

By the same token: ACTION!-bearing variables should not be null, either. If you have a variable which can either be an action or not, setting it to null isn't good because then if you reference it and its null it will just pass by what might have been intended to be a call.

Disengaged ACTION!-variables should be either TRASH! or voided, and I think voided is the better choice. Because the :foo notation is likely to be arising as a way to get nulls from things that aren't there, as opposed to things that are trash. So you could say :foo/ and get either a null or an action back, which could be tested with logic as if :foo/ [...] and also propagate assignment via bar: opt :foo/ (or if try foo/ [...] and bar: opt try foo/)

We might imagine declaring action variables as /foo in the dialect and getting them unset.

Also, SET-BLOCK! Should Work As In LET

When you use an @ in a SET-BLOCK!, things that scan that SET-BLOCK! for words (top level set-words, lets) assume you want to exempt it, and reuse the existing binding:

let start
(let [@start end]: find "abcdef" "cd", ...)
; start is "cdef", "end" no longer defined outside of GROUP!

So that didn't make a new START variable inside the group, it referenced the existing one.

The same idea is used with loop variables, to not create a new variable inside the loop scope, but use an existing one.

let x: null
for-each @x [a b c] [...]
assert [x == 'c]

So in the CONSTRUCT dialect we likely expect the same:

let start
{
    [@start end]: find "abcdef" "cd"
}

This object would have only one field, end. "cdef" would be assigned to the pre-existing start variable.

1 Like

This idea sounds good on paper, but has a problem in the traditional model.

R3-Alpha's MAKE OBJECT! process was layered like this:

  • Scan for top-level SET-WORD, collect them on the stack
  • Use those stack variables to make an object of appropriate size with appropriate key names
  • Bind spec block to newly created object
  • EVAL the spec block with bindings

This has the benefit that from the very beginning you have an object you can refer to (I was suggesting we allow you to access the object-being-constructed as . so you could access its fields as you go, as well as things in the enclosing scope with the same names).

HOWEVER, we're trying to let a lone WORD! be a field too. The evaluator is not psychic, so it doesn't know how to read:

{ foo baz bar: mumble frotz splat }

If it's going one expression at a time, it can realize that this is equivalent to one of:

{ foo baz bar: (mumble) frotz splat }

{ foo baz bar: (mumble frotz) splat }

{ foo baz bar: (mumble frotz splat) }

You can know this because you're evaluating a step at a time and figuring it out. But there's no way to scan in advance without evaluating and say which it will be. (If you're thinking you can detect arity and calculate it, then me explaining why that won't work is beyond the scope of this post. Trust me, you can't.)

This means you won't have the proto-object available while evaluating the expressions for the fields. You're accruing state to build the proto-object, and then finalizing it.

Q: Mechanically, could you simulate an OBJECT! reference that's actually virtually referencing the pending construction state, that just doesn't know about any fields it hasn't seen yet?

A: :thinking:

Yes, but that sounds difficult. I don't know if it's actually as difficult as it sounds (Ren-C is full of more challenging things that have been done.)

There's an "oh what a tangled web we weave, when we practice to deceive" aspect of it. If you tried to pass this thing off as an OBJECT! when it's a HALF-FORMED-OBJECT! then all the ways in which it's not an OBJECT! become a liability.

What's More Important: Expressive Fluidity Or .FIELD Access?

I have a fairly visceral response to this which is to say expressive fluidity

If you really think you need to access a prior field's calculation for reading purposes, you can just store the calculation in some intermediate variable.

Instead of:

{ 
    foo: some-function-that-assigns-whatever 10
    baz: whatever + 20
    bar: .baz * 30
}

You can write:

let calculation
{ 
    foo: some-function-that-assigns-whatever 10
    [calculation: whatever + 20]
    baz: calculation
    bar: calculation * 30
}

As for writing, that depends on whether we tolerate duplicate fields or not.

let calculation
let calculation2
{
    foo: some-function-that-assigns-whatever 10
    [calculation: whatever + 20]
    baz: calculation
    [calculation2: calculation * 30]
    bar: calculation2
    baz: calculation2 / 40  ; second definition and write to baz
}

Traditional MAKE OBJECT! lets you have freeform code that duplicates fields, but it kind of feels like a bug in this dialect.

Weird circular dependencies of this kind are a fairly insane thing to want to do, and I think I'm willing to say that if you're doing something like this you want to use the more traditional MAKE OBJECT! which is not dialected.

So all told, it looks like maybe access to the object currently being built is something the dialect just doesn't do.

On the plus side, this means it can be more efficient. It can walk the spec, pushing keys and values as it goes, writing them all with their finalized values in one fell swoop.

Revisiting the question above:

What's More Important: Expressive Fluidity Or .FIELD Access?

I think here, the answer is clearly the fluidity.

I'm not completely against the idea of having some way of accessing the fields of the object being built, but maybe you can't actually get the object itself.

In other words, maybe it assigns . to some mysterious thing which is able to resolve fields from the pending object construction state...but refuse give you a specific answer for what . itself is.

1 Like