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, BLOCK! Isotopes 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 this with BLOCK! isotopes. For instance, SPREAD produces them:

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

  • It gets rid of an instance of /ONLY, which has always been something that causes head-scratching to new users ("only what?")

This is now in sync with APPEND's default of not 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. I really believe this is the better behavior for people who are writing and reading the code.

The (...) Slots 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.

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 groups before you splice them.

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

compose2: adapt augment :compose [/only] [
    if not only [
        predicate: func [group <local> product] [
            either any-array? product: eval group [spread product] [:product]
        ]
    ]
]

We added the /ONLY refinement, and if you don't use it then it adds a processing function for arrays. This gives you the historical result!

:surfing_man:

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

2 Likes

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

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

UPDATE: This feature wish has been granted!

2 Likes

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

2 posts were split to a new topic: ((Doubled Groups)) as a Dialecting Tool