Ren-C Binding In A Nutshell

There is a foundational truth about code composition:

The system cannot be psychic and know if you want meaning to come from the place
where you are building the code -or- from the context in which the code will be eventually interpreted.

Ren-C has the power to talk about both

  • If you leave the code unbound ('quoted) then it will gather the meaning from where you wind up evaluating it.

  • If you BIND the code then it picks up meaning in the context of composition, and the later usage won't override it. (Note: BIND is an arity-1 operation in Ren-C, and other operations are used to bind to something other than the "current" evaluation context.)

No psychic evaluator required.

>> make-unbound: func [x] [return '(x)]

>> make-bound: func [x] [return bind '(x)]

>> x: 1020

>> make-unbound 304
== (x)  ; unbound

>> make-bound 304
== (x)  ; bound (into make-bound func instance's context)

>> '[1 +]
== [1 +]  ; unbound

>> bind '[1 +]
== [1 +]  ; bound (into console context where x: 1020 lives)

>> [1 +]
== [1 +]  ; bound (`[...]` and `bind '[...]` both act the same, by design)

>> eval append [1 +] make-unbound 304
== 1021

>> eval append [1 +] make-bound 304
== 305

Binding Has A Shorthand: $

Because control over binding is so fundamental (and needed so frequently), there's a special "binding quote" decorator, called a "TIE". This gives you a briefer way to bind things:

>> bind 'x
== x  ; bound

>> $x
== x  ; bound

All bindable types--$WORD, $TU.P.LE, $CH:A:IN, $PA/TH, $[BL O CK], $(GR O UP), ${FE N CE}--have a TIED! form.

TIED! values aren't just a good shorthand for binding quoted material in the evaluator, but also crucial dialecting parts.

In addition, a standalone $ is just a synonym for BIND.

>> $ 'x
== x  ; bound

You can make a TIED! value from a plain one with TIE, and remove the decoration with UNTIE.

>> item: tie '[a b c]
== $[a b c]  ; unbound

>> untie item
== [a b c]  ; unbound

(More general mechanisms for decoration exist as well.)


Unbound Code Becomes Useful Currency

This is working Ren-C code, that shows off doing something that people used to wish they could do (but couldn't!):

>> doubler: func [x] [
      let code: copy [add x]
      append code to word! "x"
      print ["Doubled:" eval code]
      return none
  ]

>> doubler 10
Doubled: 20

Note that from the evaluator's point of view, ADD and X inside the code block aren't bound... so the X you added being unbound made it no different.

In fact, none of the code in the function body is bound! The binding trickled down as the evaluator descended... and when the unbound block [add x] was evaluated, it synthesized a bound BLOCK!, which wound up in the CODE local variable...

But on each successive function call this process will happen again--meaning (crucially) that the [add x] that lives inside the function body never gets bound.

This stands in contrast to what would happen if the function body was built by something like a COMPOSE, where [add x] had been bound. In which case: that binding would not be overridden by the "trickle-down" of the evaluator... and the block would be interpreted with the meaning the person who composed it in intended.

Let's see that in action:

>> x: 1000

>> doubler: func [x] compose [
      let code: copy ($[add x])
      append code to word! "x"
      print ["Doubled:" eval code]
      return none
  ]

>> doubler 10
Doubled: 2000

Works as written (although it didn't end up making use of the X that was passed!). So let's do a little twist with a laser-focused COMPOSE with just the X bound outside:

>> x: 1000

>> add1000: func [x] compose:deep [
      let code: copy [add ($x)]
      append code to word! "x"
      print ["Added a Thousand:" eval code]
      return none
  ]

>> add1000 20
Added a Thousand: 1020

String Interpolation Becomes Possible

Once you can connect unbound words with values while evaluating, you have the necessary ingredient to accomplish string interpolation:

>> num: 1000

>> compose "Hello there (num + 20), World!"
== "Hello there 1020, World!"

This is a drop in the bucket compared to what you can accomplish with your own string interpolation constructs.


Addresses Key Flaws in the Rebol2/Red Binding Model

Historical Redbol's binding was done in a wave at LOAD-time, deeply on the entire code...providing the baseline for library functions (PRINT, etc.) or global variables. But doing this a-priori meant stray bindings were applied everywhere, and had to be overridden.

As a simple example, locals in functions:

Rebol []

foo: 10  ; global foo

bar: func [foo] [
   return foo + 20  ; FUNC receives body with this FOO bound to global foo
]

In this model, things like FUNC would have to override the LOAD-time binding it receives on the FOO in its body. You face questions...

  • What if you have a binding to something called FOO in a body of a function, but the code was generated... and it meant its notion of FOO, not the local?

  • What if important definitions come along later--after the initial "wave" of binding?

There were epicycles of this bad idea in things like locals-gathering FUNCT

The new rule is simply this:

If you only put bindings on when you mean them, you're not in a position of having to decide "is this binding meaningful or not"... if something has a binding, it's meaningful.

It doesn't resolve every tricky binding question that comes up...there are still plenty of hard situations to reason about. But as a basic premise, this has been serving Ren-C very well...cleanly enabling things that were intractable before.

1 Like

Note that quoting doesn't unbind things in the evaluator (though sometimes in dialects, it can be interpreted as a request to unbind something!).

Evaluating a quoted item just doesn't disrupt the binding of the thing it quotes:

>> x: 10

>> var1: $x
== x  ; bound

>> code: compose ['(var1)]
== ['x]

>> var2: eval code
== x  ; bound

>> get var2
== 10

While quoting doesn't influence binding, you'll often experience quoted code as unbound... because all code starts unbound at TRANSCODE time.

So if you're going to get bound material from a quoted thing, that quoted thing has to have been produced by some compositional process, not just LOAD-ed normally.

1 Like

Thank you @hostilefork. This new model is a godsend for me in the dialect work that I’m working on. In many places in QUERY I am parsing input expressions and translating them into executable ren-c code. This involves constructing inert code pieces and parts from different functions, which are then carefully assembled and slotted into a block for evaluation.

Merry Christmas!

2 Likes

When I first read the code behind your Query dialect, I shook my head...

:man_facepalming:

"He doesn't know..." I thought. "He doesn't know NOT to do it that way, because he thinks this is the sort of thing Rebol is supposed to be able to do. And it IS supposed to be able to work in this domain of mixing fragments together and blending that with expressions from the user... but it can't, because it's broken... and nothing non-trivial works besides on narrow cases you specifically hack it up to handle."

So it was certainly a motivator for the new model, of letting you say "here's the part of the binding I know and want to harden, here's the part I need to defer to context." Things are certainly in a better place now!

With that said...

...it's still important to remember that even with Ren-C's advancements: one must always be on the lookout to avoid magical thinking. Merely being able to mix and match parts--with all the parts maintaining the "pointers" you intended--does not guarantee semantic coherence when the parts weren't holistically designed to work together.

Hopefully I can have a screen-sharing session with you sometime, where we go over the current design of Query. We can of measure what the code generation approach is buying, and throw some hardballs at it to ask: "if it's being written in this style, what's it supposed to be enabling, and is it doing that?"

UPARSE has passed that vetting with flying colors, in part because it embraced existing parser-combinator art and extended it. I think QUERY can too...if we draw upon some established patterns from things like Linq or OSQuery, then look for what distinctly Rebol-ish advantages we can add in.

you only put bindings on when you mean them

:clap:

It's a solid foundation for code to start with no binding (context level zero), then apply bindings as program semantics require - the flexibility to "paint on some bindings" and have them respected. It's great achievement. I think I would like it much if or when I was to use it.

Since the last step of binding is applied during evaluation I wonder (in a hot shower sort of wondering) if there are other as yet unexplored possibilities. Alan Kay's original vision of object oriented programming, that to me was akin to Rebol code being passed as messages and being evaluated in a context specific way, keeps coming to mind. In Rebol this is the idea of dialect and we got Parse, but while that's fine for communicating externally, it's klunky for programming internally as components of a system. So I keep thinking if binding and evaluation can be more tied to context, I don't know but maybe evaluation can be painted on like binding, or using something like name-of-context [stuff-to-evaluate] which look like a function call but maybe isn't. I admit this could be completely useless meandering thoughts or you've already got it covered and I haven't put the dots together. [time to step out of the shower and go back to photography]

Anyway, great work!

2 Likes