BLOCK! and OBJECT! Parity in Pathing/Picking

Rebol didn't have any great answer for "object literals" and "map literals". Yet literal objects ("dictionaries") are critical to JSON. So it has seemed that Rebol needs an answer here.

Notationally we're at a bit of a loss of how to represent them. But something I began to wonder is if Rebol has been losing some of its advantage of having a SET-WORD! type. If you look at:

array: [foo <baz> #bar]
dictionary: [field1: ... field2: ...]

Couldn't it just be that the presence of SET-WORD!s in the dictionary-like thing is what gives you the tip-off? Why exchange files with literal objects, instead of doing it with blocks? The kinds of data exchanges we see using such forms are not rigorously glued to a schema--by design--the concept is that the sender and recipient are on the same page about things.

There may be little benefit to having another container type saying "this is an object", if objects and blocks act more or less interchangeably when used with keyed access. The user of the data would have the option to make a block into an object if they needed to, or not if they didnt?

I've suggested before that I feel there's something wrong with "pickquality", where it's the idea that picking and pathing consider WORD!, SET-WORD!, ISSUE!, etc. to be equal. I felt that if BLOCK!s are used for keyed access in this way, they should use SET-WORD!s...and then the seeking process in the block can look specifically for them. If we use "scant evaluation", then this could even allow for SET-WORD! values.

Demonstration

block: [
    <meta-tag 1> <meta-tag-2> #meta-issue
    string: "hello"
    x: y: 100
    code: '(1 + 2)
    setword: 'something:
]

>> block/string
== "hello"  ; skips info before set-word!

>> block/x
== 100  ; advances to first non-set-word value

>> block/code
== (1 + 2)  ; "scant" evaluation, removes quote

 >> block/setword
 == something:  ; also "scant" evaluation

Imagine being able to manipulate the block in a way that has parity with how it would act if it were an object. So getting the quoting if necessary:

>> block: [thing: 10]

>> block/thing: first [a b c]

>> block
== [thing: 'a]

You don't want path access to trigger an actual evaluation of something that isn't inert or quoted, so that should give an error:

 >> block: [thing: (1 + 2)]
 >> block/thing
 ** Error: keyed access of block only works on inert and quoted types

To me, this really feels like a coherent direction, with something that has been very lacking in coherence historically.

Cowpaths

@rgchris very much likes the freedom in blocks to say [a 1 b 2] and not [a: 1 b: 2]. So his direction of cleanup of the historical behavior would be to treat blocks as collections of 2-element records, which would prevent confusion if you wrote [a b b 2].

I appreciate wanting to have this choice. But for pathing, I like this parity with objects idea. I do not feel like enforcing the SET-WORD!s at odd numbered positions feels like the right choice there...I'd like to be able to be more free-form, as I would be able to do with MAKE OBJECT!. It addresses several dark corners, and I think that it will make blocks stronger for data exchange.

Pathing and selection are already not the same (as block/2 gives you the second item of the block, not an item immediately following an INTEGER! 2 inside the block). If we changed SELECT to a /SKIP default of 2, and went with the SET-WORD!-based behavior for pathing (hence also PICK+POKE), would that make everyone happy?

>> select [a b b 2] 'a
== b

>> select [a b b 2] 'b
== 2

And I'd suggest that SELECT would have you use the data type you were selecting on. So it would not consider SET-WORD! and WORD! the same (nor ISSUE! and WORD!, etc.) With skippable functions to do the equality, you could ask for varying behaviors:

>> select [a 1 10 20] .matches integer!
== 20  ; e.g. a wasn't an INTEGER!, but 10 was (1 skipped going in steps of 2)

And one of those behaviors could ask for a more lax style of comparison which considered SET-WORD! and WORD! to be equal. .synonymizes, perhaps. or .wordequals

>> select [a 1 b: 20] .wordequals first [a:]
== 1

>> select [a 1 b: 20] .synonymizes 'b
== 20

In any case, the idea is that the SELECT command is about treating a block as records of fixed size. I don't know if that means that it should not be used with objects (preferring PICK). It might be clarifying. PICK is a shorter word.

(I also don't know what the applications are to having SELECT default to 2 characters at a time when selecting from strings. But then again, I'm not really sure what sort of scenarios would get people to select out of a string in the first place, if all you get back is a single character. Should it be limited to just ANY-ARRAY!?)

Given your demonstration block, what should block/x: 7 do?

I'd not be against giving this a try—my main objection to the Rebol 2 behaviour was the unexpectedness in the [a b b c] example. It would be good to have a literate counterpart—a refinement of SELECT (and FIND) that echoes the pathing behaviour. Not quite sure what that'd be for the poking equivalent though.

PICK and POKE are the pathing equivalent...I'm not sure what you mean. What aspect would SELECT or FIND need to emulate?

If you mean "word matches set-word" I'm proposing that would be done with a comparison operator modification, that would open the doors to other things (I already dislike how FIND of INTEGER! matches both the datatype integer and instances of integer...it seems to me that this is better managed explicitly as one or the other, so find data .matches integer! would find an integer value while find data integer! would be the same as find data .is integer! (assuming lax equality defaults continue)

(And note find data '.matches or find data lit .matches would explicitly look for .matches in the data)

Good question, also what would block/y: 30 do. Presumably they'd either have to error -or- detach the set from the related instances, copying values if necessary. Also to consider: block: [final:] block/final: 10

Red recently revived old debates over whether SET-PATH! should be able to add to blocks, or if you should have to use append. OBJECT!s don't let you set or get keys that aren't there (which protects against typos), and don't let you delete keys...because integer-based existing bindings would be invalidated if those slots wound up being reused.

But I guess the thing to think about here is common use cases. Perhaps the real parallel is to MAP!, and arbitrary additions and removals should be allowed. I don't know, but either way there are more tools available to think about this.

As another point on BLOCK! and OBJECT! parity, people seemed to think you could SET an OBJECT! to a block.

rebol2>> o: make object! [x: 10 y: 20]

rebol2>> mold o
== {make object! [
    x: 10
    y: 20
]}

rebol2>> set o [30 40]
== [30 40]

rebol2>> mold o
== {make object! [
    x: 30
    y: 40
]}

Here we see a hardened assumption that objects have fields and they are ordered. Red has carried this assumption forward.

It complicates features that try to be "more than blocks". e.g. when R3-Alpha added "hidden" fields there was no definition of what SET via BLOCK! would do to it, so it just said "not allowed: would expose or modify hidden values."

But it suggests the kinship to blocks being far closer than one would think. This raises deep questions for the system. And I feel like there's no way to answer those questions without putting stakes in the ground and listing the things that must work.

(Clearly hiding fields doesn't have to work with SET. What's the justification that all bets are off then?)

I used SELECT here in the Portable Bridge Notation code, to step through items in order:

let suit-order: [♣ ♦ ♥ ♠]
; ...
suit: select suit-order suit

This advanced through the suits until the end was reached.

I also used it for dealing cards in directions. This case could start at any direction, and needed to cycle:

let direction-order: [N E S W]
;
direction: (select direction-order direction) else [first direction-order]

I was a bit torn on how to do this. I could have gone more like:

suit-state: head [♣ ♦ ♥ ♠]
suit: does [suit-state/1]
;
suit-state: next suit-state

This idea felt obfuscating; hiding the current suit in a series position just seemed wrong.

I even toyed with the idea of doing it as state transitions, if we are to think in terms of SELECT/SKIP 2 or SELECT seeking out SET-WORD! :

next-suit: [
     ♣: ♦
     ♦: ♥
     ♥: ♠
     ♠: _
]
; ...
suit: select next-suit suit

But that's just not what I was looking for. I felt in the end the suit order had to be just what it was, as suit-order: [♣ ♦ ♥ ♠]

...I don't know what it gets at, other than that what I was ultimately looking for here didn't fit either of our ideas. I really wanted to advance through things in a series in a context where FOR-EACH wasn't what I was using at that moment (because I was inside a PARSE rule that I didn't want to exit).

Food for thought.

Common issue. I'm sure we've all cycled through each of those different options trying to figure out the best fit.

It's nice to be able to do this:

direction: (select direction-order direction) else [first direction-order]

Obfuscating in this way is the Rebol way. If you're willing to pay a little for something a little less opaque, you could just say: append suite-order suit: take suite-order

Also, I've been using linked lists a fair bit of late (I have thoughts on potentially optimizing the linked list concept for tree building, but I digress):

♣: [next ♦ back ♠]
♦: [next ♥ back ♣]
♥: [next ♠ back ♦]
♠: [next ♣ back ♥]

suit: ♣
suit: (suit)/next
; had problems with this on subsequent calls, needed (get suit)/next

This makes me wonder if there should be a native for iterating.

 >> suit-order: head [♣ ♦ ♥ ♠]

 >> suit: iterates suit-order

 >> suit
 == ♣

 >> suit
 == ♣

 >> suit/next
 == ♦

 >> suit
 == ♦

Etc. In stackless this can be written in usermode as a YIELDER, with something approximating:

 iterates: func [data /cycle] [
      return yielder [/next /back] [
          forever [  ; e.g. lib/cycle, FOREVER for clarity w/cycle refinement
               if next [
                   if tail? data: my lib/next [
                       if cycle [data: head data]
                   ]
               ]
               if back [
                   if head? data: my lib/back [
                       if cycle [data: back tail data]
                   ]
               ]
               yield data/1
          ]
      ]
 ]

It could be made as a relatively efficient native. It might even use binding tricks to bind a single prototype action it to the data it iterates, so an ACTION! need not be created on each call.

You'd be in a situation where :SUIT was an ACTION! not a WORD!, which might seem uncomfortable. Though this doesn't seem that much different than iterators in any other language--they're always another type from the data they wrap.

My thought is an optimized 5-way (parent, first[child], last, back, next) NODE! type. I've been using blocks for this and has several disadvantages, including being difficult to PROBE and inefficient to traverse or convert to BLOCK!. Maintaining relationships would still be on the user, thus could be used for a simple linked list, double linked list or full-blown tree structure.

I like this idea.

Similar thought: Is there a prescribed way of declaring a block! (or similar structure) to be non-expanding, allowing it to contain only the # of elements of its defined size? If you insert into the block, the last value is automatically purged. (Maybe if you append to the block, the first element gets taken.) Optionally could declare the block with default values. Background: Sometimes I want to keep a "cartridge" of values meeting the spec of a certain length, and it may be as simple as a LIFO/FIFO list where I want to hold onto "the most recent" X things, and then just be able to immediately FORM/MOLD etc. into an output.

This is easy to achieve with a normal block, but it grows and grows and then you have to truncate it each time (not a big deal) and process it for your intended output.

Just a code-golf thought. Maybe there's a structure that already handles this though... I haven't dug into this.