GET+SET vs PICK+POKE - What's The Difference?

Historically, GET could not get a path:

rebol2>> obj: make object! [x: 10]

rebol2>> get 'obj/x
** Script Error: get expected word argument of type: any-word object none

That changed in R3-Alpha. Red followed suit:

r3-alpha/red>> get 'obj/x
== 10

Which seems like an improvement...but opened the door to something I've complained about: GET having side-effects, such as:

red>> path: 'obj/(print "Boo!" 'x)
== obj/(print "Boo!" 'x)

red>> get path
Boo!
== 10

When you say that two sequential GETs can get something completely different even when nothing has changed, that really pulls the rug out from under any generic code that wants to build upon what a GET is. Similar issues apply to SET.

How Do Pick and Poke Compare?

PICK and POKE add an extra parameter of a location to pick or poke from. But then they still have a "picker" of some kind.

This leads one to wonder if this would work, but it doesn't:

r3-alpha>> outer: make object! [inner: make object! [x: 10]]

r3-alpha>> pick outer 'inner/x
** Script error: pick does not allow object! for its aggregate argument

But there are two possible interpretations. If OUTER is something like a MAP, it could be looking up the PATH! inner/x as the key in the map. Or it could be looking up the key inner, fetching the thing in the map, and then picking x out of that.

MAP!s don't allow PATH! in R3-Alpha or Red, but if they did...we'd assume it would interpret inner/x as the key.

So Historical PICK and POKE are Strictly Less Powerful?

This makes it seem like GET and SET have the ability to do anything that a PATH! or SET-PATH! can do. But PICK and POKE can only go the last mile and ask one container about its response to one key.

Could we make a synonym for PICK, if we just GET a PATH! that we make up? Let's try that in Red:

red>> pick2: func [series index] [
          get make path! reduce ['series to paren! reduce ['quote index]]
      ]

red>> m: make map! [a 10 b 20]

red>> pick2 m 'a
== 10

red>> b: [x 30 y 40]

red>> pick2 b 'y
== 40

It appears to work, but the issue is that I'm sure these are completely different code paths. So you'll see subtly different behaviors for PICK vs. pathing where they'll be the same most of the time, but then not.

It would only make sense to have two codebases if someone could articulate what's different about "picking" and "pathing". Outside of function call dispatch with refinements I can't think of a good argument for a difference. And Ren-C uses TUPLE! instead of PATH! for conventional picks, so the tuple-based picking could truly be the same.

Not Easy To Reason About

This is all made-up stuff with really imaginary semantics. And I've come up against the hard questions like trying to make this work:

 item.(expression): default [...]

If you GET that SET-PATH! on the left to check to see if there's a value in it or not, and there isn't, then you decide to run the right hand side. Then you want to SET the SET-PATH! on the left...but without some alternate mechanism, you'll be evaluating the expression twice.

Being able to turn that item.(expression) into some sort of reusable currency that you can GET and SET multiple times without side effects is ideal. Once this was done by COMPOSE'ing that PATH!, but paths are now more restrictive in what they can have as members...so it would have to be turned into a block.

1 Like

...A Grand Unifying Theory??

I think GET, SET, PICK, POKE, PATH!, and SET-PATH! should all be running on the same basic hook. I've explained why that hook being recursive makes sense for POKE.

What if a GET has a secondary return value, which is the reusable sequence of steps?

>> obj: make object! [x: 10]

>> tuple: obj.(print "!! SIDE EFFECT !!", 'x)

>> get tuple
** Error: Use GET with /GROUPS: output to evaluate sequences with GROUP!

>> [result steps]: get/groups tuple
!! SIDE EFFECT !!
== 10

>> steps
== @[obj x]

So GET and SET can interpret that @-block as steps... basically a TUPLE! that has had its group elements processed (and hence may contain elements not typically legal in tuple).

>> get steps
== 10

>> set steps 20
== 20

>> obj.x
== 20

Preliminary Tests Of This Idea Look Promising :star2:

There's still a lot to mull over with this, but it's the cleanest-looking angle I've come up with yet.

Errors are still a bit of a puzzle. Once you've converted a TUPLE!/PATH! into one of these "steps blocks" then you've gone away from the source level of what the user wrote. The later SETs and GETs will only have the block--presumably not the path.

(Note: Though you could save it and pass it in? Maybe the original path could be cached as part of the block, as a kind of commentary used in error delivery?)

In any case, when you use the steps later you have less odds of erroring since the initial path access that returned the steps block worked. We'll have to see how it pans out.

2 Likes

In trying to think about what the fundamental pieces of this system really are, I've crept toward the idea that PICK and POKE are fundamentally cell-based operations... with no path processing.

This would mean that all path processing logic is actually driven by GET and SET, which implements itself on top of PICK and POKE.

To give you an idea of what I mean by that... let's look at a hypothetical poke of an immediate value:

>> obj: make object! [d: 21-Nov-2021/18:56:45-5:00]

>> poke obj.d 'time 12:00  ; poke receives cell bits but *not* an address
== 21-Nov-2021/12:00-5:00

>> obj
== make object! [
    d: 21-Nov-2021/18:56:45-5:00  ; no change to stored cell
]

So we see POKE has the smarts to be given the immediate value of a DATE! (which fits in 4 platform pointers), and some field (e.g. time) to produce a new DATE!. But it wasn't equipped to be able to change that original value.

It may be that this specific case should give an error if you use it without a flag showing that you know you're not changing anything.

>> poke obj.d 'time 12:00
** Error: DATE! will not be mutated via POKE, use /IMMEDIATE if this is ok

>> poke/immediate obj.d 'time 12:00
== 21-Nov-2021/12:00-5:00

But the idea would be that when you use a SET-WORD! or a SET, the translation is different:

obj.d.time: 12:00  -or-  set 'obj.d.time 12:00
=>
poke obj 'd (poke (pick obj 'd) 'time 12:00)

So in other words, it's SET that drives the process--breaking it down into atomic PICK and POKE, taking on the burden of writing back any changed cells. But a lone POKE itself would not be able to do any writeback of an immediate...as it received only a cell and not an address.

How Does This Relate To "Subcell Addressing"

I talk about cases like date.time.hour: xxx because it gives a case where date.time synthesizes a TIME! value which does not have a source cell of its own...so it has to be poked back.

It may not be too hard to accommodate such cases. The real problem would be things like an FFI abstraction, like:

>> struct.million_ints_field.10: 20

If the clause struct.million_ints_field generates a BLOCK! of a million integers out of the compressed form, and then you change the 10th one to 20...and then write the million integers back... that's a pretty inefficient way.

I've mentioned that this is not made up. Shixin's FFI tried to parrot the methodology of GOB! with its compressed size field, so that the struct would be able to tell it was being asked for (million_ints_field.10), and be able to do a GET or SET of that without blowing up into a BLOCK! of a million integers.

Trying to generalize this complicates the system immensely, and we are probably better off asking the datatypes which want such granularity to not expect the field selection mechanic to bear the design burden. Perhaps you write instead:

>> struct.[million_ints_field 10]: 20

This puts a bit of a syntax burden on those using custom datatypes. But having tried to legitimize Carl's GOB! trick / Shixin's FFI trick has led to what I consider to be more of a mess than it is actually worth.

If you think you need efficiencies out of "subcell addressing", the likely truth is that you need to break your data model into more cells.

What I'm saying here is that the "recursive" nature instead becomes a backpropagation in SET. So it can just linearly go backwards across the path it has processed.

1 Like