ENVELOP (and COMPOSE!) By Example

Prior to splices, we were considering rethinking append/only [a b c] [d e] as append [a b c] only [d e], where ONLY would just envelop its argument in a block.

@rgchris didn't care for the name:

As it happens, ONLY defined in this way stuck around for a while. (I actually thought it had been deleted, but it turns out it was hiding as **only*, so just finally deleted it now!)

I agree that ENVELOP is a better and more useful name for the category of operations. Today we have ENBLOCK and ENGROUP:

>> enblock [a b c]
== [[a b c]]

>> enblock <tag>
== [<tag>]

>> engroup [a b c]
== ([a b c])

>> engroup <tag>
== (<tag>)

But there's no generalized ENVELOP.

"Envelop by Example" Seems Like an Important Construct

>> something: 1020

>> word: 'something  ; demo behavior when unbound (binding from context)

>> envelop '[] word
== [something]

>> envelop '() word
== (something)

>> envelop '@[] word  ; would work with sigil-decorated types
== @[something]

>> envelop '(()) word  ; could work with nested envelopes
== ((something))

There's a big advantage in passing in a block or group "by example". It means you can implicitly pass along a binding, which can be integrated in the same step...if that's what you want. (The modern art of writing Ren-C code requires a lot of consciousness about the decision to use bound or unbound material.)

>> eval envelop '(()) word  ; quoting means no binding
** Error: something not defined

>> eval envelop $(()) word  ; if binding passed in, it's used
== 1020

ENVELOP might even support Synthetic Asymmetric Delimiters

>> envelop '(| |) word
== (| something |)

>> envelop '(|) word  ; shorthand--assume paired?
== (| something |)

>> envelop '(<*>) word  ; maybe not assume, for COMPOSE marker compatibility
== (<*> something)

ENGROUP and ENBLOCK Still Useful

I do think that ENGROUP and ENBLOCK as specializations of ENVELOP turn out to be what you'll use at least 90% of the time...so they're worth having around.

But as arity-1 functions, the returned block or group would be unbound at its tip. So you'd have to use the ENVELOP-by-example to pass in a binding.

This Overlaps the MORPH Proposal Somewhat

MORPH has the ability to change the decorations on the value you're passing in, whereas ENVELOP would assume you wanted the item as-is, just enclosed in some other stuff.

My instinct is to say that this takes the pressure off MORPH to be all things to all people... vs. the idea that we don't need ENVELOP and it should just become a subfeature of morph. But I dunno.

1 Like

I have realized that this is an incredibly useful ability...

...but even more importantly...

The Binding Aspect Motivates COMPOSE-by-Example

Since today's COMPOSE is arity-1, to get it to work at all you have to run it on a bound block (assuming the nested groups you're composing aren't somehow already bound). The tip of the binding of that block is what COMPOSE sloppily borrows to use when evaluating the inner groups.

>> x: 1, y: 2  ; let's say these are incidental definitions

>> var: 'y

>> code: compose '[x + (var)]
** Error: var is not bound

>> code: compose [x + (var)]  ; eval'd BLOCK! binds, compose borrows that binding
== [x + y]  ; but the result tip still has the binding

>> eval compose [let x: 10 let y: 20 (as group! code)]
== 3  ; let's say this is not what I meant

If you didn't want the final result of a COMPOSE to be unbound, you still have to bind the block long enough for compose to find the bindings...and then unbind it.

Not only is that awkward, what if you had a meaningful binding on the input you wanted to keep. You'd have to store the binding somehow... bind to the context for your groups long enough for the compose to work, then rebind it to the stored binding...

Compose-By-Example Can Fix This! :smiley:

Let's bring back an old term...and call it COMBINE.

>> code: combine $() '[x + (var)]
== [x + y]  ; worked even though we passed in an unbound block!

>> eval compose [let x: 10 let y: 20 (as group! code)]
== 30

So not only do you get the freedom to specify what delimiters (or synthetic/nested delimiters) you want to use, you can also supply an arbitrary binding.

Old COMPOSE Is Still Useful Day-To-Day

It's useful enough to keep its name, and do what it does. It works out a lot of the time.

But the strange thing here is that COMPOSE wouldn't just be a specialization of COMBINE with an unbound group '(). I think that would imply leaving the bindings on the groups as-is, not stealing the binding off of the other argument.

So COMPOSE would likely instead be an adaptation of COMBINE that would take the binding off of the thing you passed it, and put it onto the "example". Let's say the two arguments to COMBINE are PATTERN and TEMPLATE (see post on specialize:relax):

compose: adapt (specialize:relax get $combine [
    pattern: ~<removed from interface (ADAPT phase fills in)>~
]) [
    pattern: inside template '()
]

This is all quite cool. Agree, @bradrn?

There's a bit of a missed opportunity here with being able to use sigils for COMBINE...to say the sigil is enough, you don't need a list:

>> var: 'x

>> combine '$ '[x + $var]
== [x + y]  ; unbound

Besides letting you avoid lists, it could be useful with lists when you want to generalize the same compose operation across GROUP!s and BLOCK!s (and FENCE!s)

>> combine '@ '[[some stuff] @[spread [a b]] (other stuff) @(reverse [c d])]
== [[some stuff] a b (other stuff) [d c]]

There's no binding, so it won't work. BUT I WANT IT! :pouting_cat:

Could SIGIL!s Carry a Context, like an ANY-LIST Can?

It wouldn't necessarily be hard to give them list-like properties of carrying contexts, but getting the binding on them would be annoying:

>> combine (inside [] '$) '[x + $var]
== [x + y]  ; unbound

Could $$ mean "bind the $ sigil", and $@ mean bind the @ sigil, etc?

>> $$
== $  ; bound

>> combine $$ '[x + $var]
== [x + y]  ; unbound

That could work, with $$ being its own SIGIL! with this particularly strange behavior.

Note that $ being a WORD! (which it shouldn't/can't) would likely not help--because words do not today store "contexts"/"specifiers". Once they are bound, they are glued to the thing they are bound to...this is a performance point, because it means each fetch of a bound word doesn't have to look it up again. Only the binding process itself does the lookup for words. So leveraging the "not a word" nature of SIGIL! to have a weird property like being able to store contexts might make their strangeness useful vs. just strange.

I am having a hard time thinking about how it would fit into a more general mechanic... I don't know that implies $$word and $$block etc. would need to exist, and I'm 99% sure I don't want them to.

But maybe just being a magical outlier is all right. It is called "SIGIL" after all:

A sigil (/ˈsɪdʒɪl/) is a type of symbol used in magic. The term usually refers to a pictorial signature of a deity or spirit (such as an angel or demon. In modern usage, especially in the context of chaos magic, a sigil refers to a symbolic representation of the practitioner's desired outcome.

Yep, this looks good!

(Not sure how happy I am with bindings on sigils, though. That feels like it may open up the same can of worms as bindings on strings do.)