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 @ sigil on example, use example binding
== 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:

I suggest the arity-2 form be called COMPOSE2. (It's a good name because it tells you the arity, and also kind of like it's the sequel... COMPOSE 2: The Revenge)

When you pass a list with an @ on it, COMPOSE2 receives that signal and interprets it to mean you want to use the binding of that list for the substitution sites:

>> var: 'y
== y

>> code: compose2 @() '[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 with an non-@-list has to mean use the binding of the template.

compose: specialize compose2/ [template: '()]

And you can do this off the cuff...

 test: func [block] [  ; can't see foo
     compose2 '{{}} block  ; block captured environment
 ]

let foo: 1000

>> test [hello {{foo + 20}} world]
== [hello 1020 world]

:man_mage:

5 posts were split to a new topic: Fretting Over The Arity Of COMPOSE

A post was merged into an existing topic: Could SIGIL! Carry A Binding?

What About Single-Arity "Use Current Context?"

It seems like it would be nice if there were a way to say "use the current context" in a single term.

I've wondered what to call that. It could be something meaningless like COMPOSE*, or something long like COMPOSE-HERE

>> var: 'y

>> compose-here '[x + (var)]
== [x + y]  ; worked even though we passed in an unbound block

>> compose* '[x + (var)]
== [x + y]  ; worked even though we passed in an unbound block

Strings don't have context and require you to use COMPOSE2 with a bound pattern. But I've considered that INTERPOLATE would be a synonym:

>> compose2 @() "Hello (1000 + 20) World"
== "Hello 1020 World"

>> interpolate "Hello (1000 + 20) World"
== "Hello 1020 World"

(Note the important distinction here: this won't work as a specialization, because the @() has to be positioned at the callsite to capture the binding under evaluation.)

In any case, there's no reason INTERPOLATE couldn't be used on blocks. It's all based on COMPOSE2.

>> interpolate '[x + (var)]
== [x + y]  ; worked even though we passed in an unbound block

This would mean there's only one "bad boy" operation which is context-dependent minus any parameterization of that dependency. I like the idea that it's a whole separate word vs. being some dodgy refinement to COMPOSE, so you always know that if you see that word you're getting the callsite-dependent behavior.

However, it's still pretty long. If you want to PRINT and INTERPOLATE that's pretty hefty, compared to something like PRINT*

>> print interpolate "Hello (1000 + 20) World"
Hello 1020 World

>> print* "Hello (1000 + 20) World"  ; "bad boy", uses callsite context
Hello 1020 World

But I don't like the idea of every function having to name a variant like that, to get a behavior that should come from a separate call.

In PRINT's specific case, we might say that there's a difference between printing a string passed in a block vs. not in a block:

>> print "Hello (1000 + 20) World"  ; => print interpolate "..."
Hello 1020 World

>> print ["Hello (1000 + 20) World"]  ; => print spaced [...]
Hello (1000 + 20) World

I think this could actually be worth it. It's learnable, and today's PRINT is designed to restrict the types it accepts to just BLOCK! and TEXT! (and the newline character) based on the notion that print value is a bad pattern for printing arbitrary values, because the day it's a block it will suddenly evaluate.

Or maybe this could be the meaning of printing a TAG! ?

>> print "Hello (1000 + 20) World"  ; => write-stdout join "..." newline
Hello (1000 + 20) World

>> print <Hello (1000 + 20) World>  ; => print as text! interpolate <...>
Hello 1020 World

Hm, maybe that's better...

Looks Good, Just Need INTERPOLATE's Shorthand

Shorthands for interpolate are hard to think of:

>> interp '[x + (var)]  ; stilted, evokes "interpret" to me
== y

>> inter '[x + (var)]  ; looks better, but "what?"
== y

I'll keep thinking, but the PRINT behavior variation seems like a winner to me.

1 Like

Urgh, it occurs to me that if ENVELOP runs into a problem if it follows the pattern compose uses, because there's a need to distinguish using the binding vs. not. That kind of restricts it to @... and ... with no sigil. :pouting_cat:

You could still use INERT to get the @:

>> envelop '[[]] 'something
== [[something]]  ; unbound

>> envelop @[[]] 'something
== [[something]]  ; bound to same tip as the @[[]] you passed in

>> inert envelop @[[]] 'something
== @[[something]]  ; same binding, now with @ decorator

Actually, y'know, it would be nicer if JOIN took SIGIL!s

>> join '$ envelop @[[]] 'something
== $[[something]]

In fact, INERT is a little bit esoteric and not entirely accurate (given that the @FOO evaluates under binding). That's probably better all the time:

inert
join '@  ; only two more characters but clearer!

But this would be a strange spin on JOIN's behavior if it didn't reduce the argument if it was a block. Anyway, something to consider now that the binding behavior of "pattern" arguments are starting to take shape...

It occurs to me that ENVELOP could be polymorphic w.r.t. strings...

>> envelop "<* *>" "Your text here"
== "<*Your text here*>"

I'm envisioning a "dumb" behavior... just looking for the space, not enforcing any matching:

>> envelop "<* <*" "Your text here"
== "<*Your text here<*"

It does put the burden on people to join together a string vs. passing the begin and end delimiting as separate items. But I think using the fact that the pattern is a list to cue that you want a list result seems pretty good.

And then...

>> envelop "<* *>" [What "would" this "do?"]
== ???

Questions for the future. :man_astronaut:

New ideas take time to settle. But I may be getting over my squeamishness about having the default interpretation of COMPOSE be to use your current context.

So, this would be the meaning in COMPOSE2 when you pass a pattern with no markings.

>> dir: %some-dir/

>> compose2 '(()) %"((dir))/This Filename (Is Dumb).pdf"
== %"some-dir/This Filename (Is Dumb).pdf"

(Note it didn't do two slashes there. This is because I'm aiming to make COMPOSE treat FILE! composition specially in terms of impementing a kind of "filename calculus". e.g. it would not allow you to compose a FILE! in that spot unless it's a directory with a slash at the end, given that a slash follows it.)

How to get the "use binding of block" behavior simply?

So if no special mark indicates use binding from current environment, and the @ mark means "use the binding on the list I'm giving you", how do you say "use the binding of the template list at the tip"?

It's a little annoying to write manually. :frowning:

 compose2 (inside template '@()) template

But how annoying is it really? And actually, since it doesn't need a binding, we could use another part-of-speech...

 compose2 '$() template

A bit esoteric to say "$ means use binding of template" but you have to quote the binding to get it. That seems to be begging for mistakes, saying $() instead of '$() and getting the "bind here" behavior. I wish there were some kind of semiotic thing we could do like:

 compose2 [() <-] template

(I say "I wish" as if we couldn't actually do exactly that. But we can. I'm just a little squeamish about weird dialects like that in fundamental functions. Should I be?)

ANYWAY... the new idea means you can compose unbound code more easily:

 >> x: 1000

 >> compose '[Hello (x + 20) Unbound World]
 == [Hello 1020 Unbound World]  ; unbound

Anyway, like I say, it has taken a while for me to be comfortable but with interpolate working so darn well I feel that COMPOSE should be its name, and move on...

My writing on this is a bit inconsistent, but it seems to be suggesting:

>> something: 1020

>> word: 'something

>> envelop '(()) word
== ((something))  ; tip unbound, word something unbound

>> envelop @(()) word
== ((something)  ; tip bound

This seems to be "not be giving the author enough credit" to say what they mean with binding. Why not be more obviously mechanical:

>> $(())
== (())  ; bound

>> envelop $(()) word
== ((something))  ; tip bound

>> envelop '$(()) word
== $((something))  ; unbound

>> $ envelop '$(()) word
== $((something))  ; tip bound

>> @(())
== @(())  ; bound

>> envelop @(()) word
== @((something)  ; tip bound

>> envelop '@(()) word
== @((something)  ; unbound

e.g. why not take the binding of the envelopment pattern at face value, and use it as is... vs. assuming the binding is not relevant unless it carries a sigil like @

The concept of fretting over heeding the binding or not comes from worries over heeding the meaning of the binding in the pattern in COMPOSE2 on accident, vs. using the binding at the callsite.

or COMPOSE Always Heed Callsite Binding, Easy to Switch

This seems like a better idea. If COMPOSE never gives meanings to the binding on the pattern, you don't worry about it. Then all the sigil'd patterns are available!

>> compose:pattern '@() [(1 + 2) @(3 + 4) (5 + 6)]
== [(1 + 2) 7 (5 + 6)]

>> foo: 10

One particularly interesting one would be anything with a "sigil" on it, such as $ or ^ or @, affording the idea of calling out single word compositional forms:

>> compose:pattern '$ [$foo foo $(1 + 2)]
== [10 foo 3]

Maybe even quoting can get in on the action...

>> compose:pattern '' ['foo foo '(1 + 2)]
== [10 foo 3]

>> compose:pattern ''$ ['$foo $foo '$(1 + 2)]
== [10 $foo 3]

For more readability of what the pattern is:

>> compose:pattern (the ') ['foo foo '(1 + 2)]
== [10 foo 3]

>> compose:pattern (the '$) ['$foo $foo '$(1 + 2)]
== [10 $foo 3]

I think that the notion of slipstreaming the context for compose onto the pattern itself is outdated, and COMPOSE should just ignore any binding on the pattern.

Then things like ENVELOP can just honor the binding of the pattern, because that's a different situation.

1 Like

Thought One: If someone doesn't want things to look too symbol-y, we could allow the datatypes for the basic patterns too:

>> envelop block! word
== [something]

>> envelop group! word
== (something)

Thought Two: Since we can COMPOSE with strings, is there any reason you couldn't ENVELOP with them?

>> something: 1020

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

>> envelop "[]" word
== "[something]"

>> envelop "()" word
== "(something)"

It could splice things in the midpoint. Or it could assume that wherever you put a space is where it's supposed to put the thing:

>> envelop "[ ]" word
== "[something]"

>> envelop "( ))" word
== "(something))"

That seems more useful and easy to define.

Maybe not the most useful feature in the universe, but I could imagine using it. It's like a version of compose where you compose only a single thing, using space as the callout of where to compose.

1 Like