Binding in "sequences" (TUPLE!, CHAIN!, PATH!)

I was looking back at the idea of "dialected function calls"

And this reminded me that I never fully figured out what the binding strategy was for what I've called "sequences" (paths, tuples, chains).

First I'll cover some historical points...

Rebol2: PATH!-ing, SELECT-ing ignores binding

Looking at Rebol in general, there's no meaning to the binding when doing a PICK:

obj1: make object! [a: 10]

obj2: make object! [a: 20]

So you can use unbound material:

rebol2>> select obj2 to word! "a"
== 20

And binding the material somewhere else doesn't matter:

rebol2>> word: bind 'a obj1  ; uses reverse arg order in bind
== a

rebol2>> get word
== 10

rebol2>> select obj2 word
== 20

In Rebol2, building a PATH!, keeps the bindings as in any "list":

rebol2>> path: to path! reduce ['obj2 word]
== obj2/a

rebol2>> probe get path/1
make object! [
    a: 20
]

rebol2>> get path/2
== 10

Though the bindings are there, only the binding of the first item matters:

rebol2>> do path  ; GET doesn't handle PATH! in Rebol2
== 20

R3-Alpha/Red: GROUP!s Muddy The Waters

Allowing GROUP!s in PATH!s was something DocKimbel considered to be a mistake for various reasons. Red still doesn't support it at LOAD-time at heads of paths:

red>> load "foo/(second [bar baz])"
== foo/(second [bar baz])

red>> load "(first [foo bar])/baz"
== [(first [foo bar]) /bar]  ; not (first [foo bar])/baz

You can manually construct a path with a GROUP! at the head:

red>> foo: make object! [baz: 304]

red>> path: to path! [(first [foo bar]) baz]
== (first [foo bar])/baz

Although Red rejects it in evaluation:

red>> do path
*** Script Error: path must start with a word: (first [foo bar])/baz

R3-Alpha doesn't error, it just gives weird/wrong answers:

r3-alpha>> do path
== (first [foo bar])/baz  ; huh?

r3-alpha>> get path
== none  ; huh?

Those oddities aside, both support the case of evaluations in non-head positions, in their ways:

red>> path: to path! [foo (second [bar baz])]
== foo/(second [bar baz])

red>> do path
== 304

r3-alpha>> path: to path! [foo (second [bar baz])]
== foo/(second [bar baz])

r3-alpha>> get path
== 304

Point being: Binding Matters in non-head positions at minimum when you have GROUP!s.

Ren-C introduces many more angles. There are three "pathlike" interstitially-delimited types...

  • PA/TH
  • CH:A:IN
  • TU.P.LE

They're based on common mechanical behaviors of a "sequence" (but distinct from the common "list" mechanics used by [BL O CK] and (GR O UP) and {FE N CE}). The rules block the creation of sequences of fewer than two elements, and enforce other construction constraints (e.g. you can put TUPLE!s inside of PATH!s, but not vice-versa). There's also no index position in a sequence, they are always implicitly at their "head". This avoids some classic issues:

rebol2>> path: 'a/b
== a/b

rebol2>> path: next path
== b

rebol2>> path: next path
==

rebol2>> path
==

In order to achieve their constraints, Ren-C sequences are "immutable". Classically this has been a shallow immutability, and not a deep one:

>> tuple: join tuple! ['foo '(xxx second [bar baz])]
== foo.(xxx second [bar baz])  ; unbound

>> take tuple
** PANIC: take expects [<opt-out> any-series? port! varargs!]

>> tuple.2
== (xxx second [bar baz])  ; unbound

>> take tuple.2
== xxx  ; unbound

>> tuple
== foo.(second [bar baz])  ; unbound

This means that when you put a series inside of a sequence, it retains its identity and doesn't get locked down deeply. I don't know if this is the right answer, but it's the answer of today.

Important to notice is that in the code I wrote above, the path has no binding at its "tip", nor do the elements have binding.

 >> foo: make object! [baz: 1020]

>> tuple
== foo.(second [bar baz])  ; unbound

>> get tuple
** PANIC: Couldn't get binding for foo

>> eval tuple.2
== PANIC: Couldn't get binding for second

You can bind the tuple at the tip, and it will be able to work (although you have to use :GROUPS with GET, because to avoid "surprise evaluations"... though this may be overkill. You may want to specialize GET with :GROUPS to always support groups.)

>> $ tuple
== foo.(second [bar baz])  ; bound

>> get:groups $ tuple
== 1020

Similarly you can bind the group itself and evaluate that:

>> eval $ tuple.2
== baz  ; unbound

This leads us to ask... what if we had built the TUPLE! with the GROUP! having its own binding? Let's be sneaky:

>> foo: make object! [baz: 1020]

>> group: (let second: probe/, $(second [bar baz]))
== (second [bar baz])  ; bound

>> tuple: compose 'foo/(group)
== foo/(second [bar baz])  ; unbound

All right, now what do you expect from this?

>> get:groups $ tuple
[bar baz]
** PANIC: can't use BLOCK! [bar baz] to pick from OBJECT!

So here we see the binding of the tip we have to $ tuple being overridden by the binding context that was captured when we said $(second [bar baz]) inside the scope of a LET that had defined SECOND as PROBE, and displayed [bar baz] instead of picking baz.

Seems coherent so far, right?

This is just based on the premise that in the wave of evaluation, things that already have a binding keep it...and things that don't have a binding assume the "current context".

(Will there ever be an idea of the "current context" having extra instructions to merge or override an existing binding? I don't know. There's nothing built in to do that.)

What About "Wordlike" Sequences?

There's a special compression applied, to things that are considered "wordlike"... e.g. a two-element sequence of a WORD! and a BLANK! (the blanks don't render):

  • /foo and foo/
  • .bar and bar.
  • :baz and baz:

This compression fits the sequence into a single cell, and is a very important optimization.

Though it raises the question: "what if the word and the sequence have distinct bindings" :frowning:

That would permit 4 different possibilities:

  1. The overall sequence is unbound, and so is the contained word
  2. The sequence has a tip-binding, but the word is not bound
  3. The sequence has no tip-binding, but the word is bound
  4. Both the sequence and the word are bound

In such a world, you could wind up with situations like:

>> chain
== baz:

>> chain.1
== baz

>> get chain.1
** PANIC: baz is not bound

>> get chain
== 1020

Or:

>> get chain.1
** PANIC: baz is not bound

>> get chain
** PANIC: baz is not bound

Or:

>> get chain.1
== 1020

>> get chain
== 1020

etc, and it gets pretty confusing, and would break the optimization if we have to store two bindings per cell.

Could Sequence Creation Prohibit/Discard WORD! Bindings?

A WORD!-optimized sequence today doesn't "discard" the binding, it just blurs the distinction--the sequence and the word both have the same binding. e.g. chain and chain.1 have the same binding always.

The general case of disallowing bound words in sequences doesn't really make sense, e.g. this should work:

>> data: [a b c]

>> tuple: join tuple! [$data 2]
== data.2  ; no tip-binding

>> tuple.1
== data  ; bound

>> get tuple.1
== [a b c]

>> get tuple
== b

So I think if you create one of these wordlike sequences, you might just have to accept that the binding of the sequence matches the binding of the contents.

Maybe there could be a bit which tracks whether a tip-binding has been requested or not or if the content is bound, and so you get bound or unbound answers... but whatever answer you get will always match?

Back to the Provocation...Dialected Function Calls

What had me sort of "hmming" about this would be the question of if you were constructing code programmatically, and you could only bind the CHAIN! sequence at the tip without having a distinct binding for fail and $var the way you could when building an ordinary list.

Facing the realization that it's essential to store the bindings of things like GROUP!s in sequences that may be distinct from the tips, I think this points to the idea that compression probably shouldn't be throwing out the bindings of "wordlike" elements in the general case.

BUT in the reduced case of where a sequence consists of one-and-only-one wordlike element, is there harm in equating the binding of the sequence with the wordlike thing? As I point out, current evaluation rules are such to say that you don't override it.

Sequences are immutable, and if you create one with an unbound word in it then it will always be unbound. I think maybe this can be covered by one bit: is the binding on the cell that of the sequence or of the word. In almost all cases, the word will be unbound and it will be the sequence's binding. Then in the rare case where you're asked to add a binding onto a wordlike optimized sequence whose binding belongs to the word, you deoptimize it.

OR... wait. As it actually happens, WORD!s currently have an unused slot...reserved for optimizations. This could be where if the word has a distinct binding from the containment, it gets stored and extracted by the optimization code.

Long story short, I think I've sorted out my conceptual problem here: interstitial sequences need to honor binding just like lists do, the optimizations that have been dishonoring it just have to become smarter.

1 Like