The Superpowers of Ren-C's Revamped COMPOSE

COMPOSE is one of Rebol's most useful and recognizable constructs. As with most-things-Ren-C, it has evolved to become far more powerful.

No Splicing By Default, Use ((...)) To Request Splicing

One of the immediately noticeable differences is that there is no /ONLY option any longer. A plain GROUP! will not splice its evaluative result. So if block: [a b c], it is different from historical Redbol:

rebol2/r3-alpha/red>> compose [value: (block)]
== [value: a b c]

ren-c>> compose [value: (block)]
== [value: [a b c]]

If you want to have something splice, you do it with the specific notation of a doubled-group:

ren-c>> compose [value: (block), ((block))]
== [value: [a b c], a b c]
  • You can mix and match splicing parts and non-splicing parts in the same COMPOSE!

  • There's a strong visual signal that multiple-elements might be spliced in.

    • It's intuitive as well, because ((...)) is "fatter" than (...).
  • It gets rid of an instance of /ONLY, which has always been something that causes head-scratching to new users ("only what?")

While this deviates from APPEND's default of splicing, I am convinced that practice has shown that not splicing is a safer and more intuitive default. You're so often dealing with splicing several values at a time that the odds of you knowing precisely what data type all of them are become lesser. As a higher-level tool, it should cater to the higher-level needs...and that's not splicing unless you ask. I really believe this is the better behavior for people who are writing and reading the code.

Both (...) and ((...)) will Vaporize Voids!

In my list of "non-negotiables", I've always said this had to be true, somehow:

>> compose [<a> (if false [<b>]) <c>]
== [<a> <c>]

For quite some time, a conditional like IF that didn't run its branch would return NULL. Because NULL couldn't be put in a block, it seemed like it was a good fit for signaling vaporization of a clause in a COMPOSE. But I was nervous because as NULL came to mean "soft failure", this felt like you could be sweeping a failure under the rug.

Yet it was heavy-handed to need to say something like (maybe if false [<b>]) to turn the NULL into a purely invisible VOID. So it was a relief when the mechanics of "impure invisibility" allowed conditionals like IF to safely be void instead of null when a branch did not run.

The result is the best of both worlds, where NULL can give a specific error tied to null splicing.

>> compose [<a> (select [a 10 b 20] 'c) <c>]
** Script Error: non-NULL value required (see MAYBE, TRY, REIFY)

>> compose [<a> (maybe select [a 10 b 20] 'c) <c>]
== [<a> <c>]

(Trust me, this turns out to be powerful--not a hassle. I'll show exactly how insaneo-style that is in an upcoming demonstration, so be looking forward to it. :sunglasses: )

Decorated Groups (including quoted) Apply Their Decorations!

Ren-C has a lot of variations of GROUP!:

COMPOSE does useful magic for you, and if the type a GROUP! evaluates to supports the decoration, it will be applied!

>> fruits: [apple banana]
>> dispositions: [favorite least-favorite]

>> compose [(dispositions.1): '@(second fruits)]
== [favorite: '@banana]

Once you have this, you won't want to go back. The premise of the language is being able to dynamically play with code and generate structures on the fly. This makes that feel extremely seamless.

One little interesting thing to observe is that although you can't splice nulls, you can splice a quoted NULL:

>> compose [''(second [a])]
== ['']

These little details turn out to be important: if you look closely at the implementation of something like the console you will see how they permit no-hassle handling of what may seem like difficult edge cases!

You Can Label the Specific Groups You Want Composed!

One of the good things about templating is to be able to write most of your code normally, and then point out just the parts you want to substitute. So if you're using so many groups that just being in a GROUP! isn't distinguishing what you want to substitute, tagged COMPOSE to the rescue:

You can pick whatever label you want, and the first item of each group will be checked against it:

>> compose/label [(1 + 2) (<*> 1 + 2) (1 + 2)] <*>
== [(1 + 2) 3 (1 + 2)]

As a neat shorthand for this, a skippable TAG! parameter can be used:

>> compose <*> [(1 + 2) (<*> 1 + 2) (1 + 2)]
== [(1 + 2) 3 (1 + 2)]

The TAG! has to be given literally between the COMPOSE and the expression you want to compose. (This is a requirement for <skip>-ability.

Doubled-groups still have /ONLY semantics:

 >> x: [a b c]

 >> compose <$> [(1 + 2) (<$> reverse copy x) ((<$> reverse copy x)) ((1 + 2))]
 == [(1 + 2) c b a [c b a] ((1 + 2))]

You don't have to use symbols...any tag will do. Could be a whole word with meaningful names, which might be valuable if you were doing it in several steps...where earlier phases could leave tags for later phases to compose. You might also tag with numbers, <1> <2>...

Predicate Functions Can Process the Compose Slots!

We now have the ability to run functions on the slots before you splice them. These functions are specified via the "predicate" convention (for the moment it's a "refinement-style" PATH!, but will be a TUPLE! when generalized tuples are implemented)

In order to allow your return result to ask for splicing or not, you return using the typical conventions for APPEND. e.g. a quoted value will not splice...

>> nonsplice-hook: function [x] [quote reduce [(x + 1) (x + 2)]]

>> compose/predicate [(10 + 20) (30 + 40)] :nonsplice-hook
== [[31 32] [71 72]]

But a non-quoted value will splice:

>> splice-hook: function [x] [reduce [(x + 1) (x + 2)]]

>> compose/predicate [(10 + 20) (30 + 40)] :splice-hook
== [31 32 71 72]

This leads up to revealing a clever design trick about what ((...)) GROUP!s are actually doing...

...they don't specifically mean "don't splice", they are asking to not apply the predicate...where the default predicate for plain (...) groups is simply META!

So what ((...)) does is unaffected by the predicate:

>> nonsplice-hook: function [x] [quote reduce [(x + 1) (x + 2)]]

>> compose/predicate [(10 + 20) ((reverse [a b c]))] :nonsplice-hook
== [[31 32] c b a]

So if you want to define COMPOSE that acts historically like Rebol2 (splicing unless you say /ONLY), here's how you can do it:

compose2: adapt augment :compose [/only] [
    if not only [
        predicate: :blockify  ; in block if not already, only blocks splice

We added the /ONLY refinement, and if you don't use it then it will put anything that isn't a block already into a block. Since the result of the predicate is processed using "APPEND semantics", this give syou the historical result!


This only scratches the surface of what's possible, with these bendable and useful ergonomics.


Very useful!
Maybe also the FILE! type could be enabled:

compose % [(1 + 2) (% 1 + 2)] ...

UPDATE: This feature wish has been granted!


3 posts were merged into an existing topic: LkpPo (Stéphane)

Scrumptious enhancements here! :tongue:
I've used COMPOSE quite a bit in my code previously and was grateful for it, but it was just a normal rebol way of getting things done. The labeling and predicate changes make it much more likely that I'll be building a lot more of my code with this powerful function.

1 Like

One qualm I have about this is the visual difficulty of telling if something is a double-group if you are composing more than one item.

>> compose [a b ((some code
         that goes on and on and) oh it ended there (maybe you start
         another group and
     eventually end)) d e]

That's not a splicing case because it's not a double group, but it kind of looks like one.

But it's hardly the only case where if you jump to conclusions on reading a few characters that you'll have problems because the code keeps going and may invalidate the assumptions you made by looking at those few characters.

I'm going to say we should continue to assess this in real-world cases. Mine so far bear out that I usually want compose to just put values in as-is, and I find value in annotating the places that's not true.