All-SPLICE!: REDUCE, DELIMIT (SPACED, MOLD...)

COMPOSE was a sort of poster child for the use of SPLICE!, so that you could mix and match splice cases with non-splicing ones... carried by the values, with no concern for /ONLY:

>> block: [a b c]

>> compose [(block) d e (spread block)]
== [[a b c] d e a b c]

But when suggesting the more-obviously-a-verb word SPREAD for the operation, @rgchris gave this example for REDUCE behavior, which hadn't occurred to me previously:

>> reduce [spread [a b c] [a b c]]
== [a b c [a b c]]

I'm guessing most people would be in favor of having the splicing behavior. Arguments that say that there should be a 1:1 correspondence between expressions and values in a REDUCE are already pretty much out the window, since VOID elements vanish (including conditionals that don't take any branch).

So I implemented this! (Circa October 2022)

SPLICE! Handling Is In DELIMIT & Friends Now, Too...

I've complained in the past that the often random-seeming treatments of blocks in Rebol2 functions like REJOIN lead to problems--and that it would be better if people had to be explicit about their intent. This offers the ability to "inherit" whatever the enclosing delimiting strategy is, and fold into the existing operation (technically more efficient):

>> block: ["c" "d"]

>> spaced ["a" "b" block]
** Error: BLOCK! not handled by DELIMIT, use SPREAD or desired string conversion

>> spaced ["a" "b" spread block]
== "a b c d"

>> spaced ["a" "b" unspaced block]  ; if you wanted another interpretation
== "a b cd"

I believe I prefer this over having some default way that blocks behave inside string conversions. The odds of guessing right are low enough that it's better to have people be explicit.

4 Likes

3 posts were split to a new topic: Does SPREAD of a BLOCK! Affect Other References?

And What About MOLD, You Ask?

Historically, the way you would ask to mold an array without its delimiters was with MOLD/ONLY:

rebol2>> mold [a b c]
== "[a b c]"

rebol2>> mold/only [a b c]
== "a b c" 

In Ren-C this raises some existential questions, like how should quoted arrays be handled?

mold/only first [''(a b c) d e]  ; ???

But I think there's a better answer: use SPLICE! antiforms. If you want to mold the contents of an array, then turning it into a splice seems the natural answer. And since you can't have splices of quoted things...there's a nice unambiguous answer.

>> mold [a b c]
== "[a b c]"

>> mold spread [a b c]
== "a b c"

It does raise the question of what to do if you have something that might be an array or might not. How do you tell it to mold as is if it's not an array, or without the delimiters if it is? That's what MOLD/ONLY did, after all:

rebol2>> mold/only [b l o c k]
== "b l o c k"

rebol2>> mold/only <tag>
== "<tag>"

"SPREAD won't SPREAD tags..." you say. And no, it won't. But I think this is a rare case... and the neat thing about putting the bit on the value (as opposed to a refinement) is you can make functions like SPREAD-OR-AS-IS. Or SPREAD-IF-PATH-OR-GROUP. You can really tweak this however you want.

>> mold spread-or-as-is <tag>
== "<tag>"

>> mold spread-or-as-is "[b l o c k]"
== "b l o c k"

>> spread-if-path-or-group 'p/a/t/h
== \~[p a t h]~\  ; antiform (splice!)

>> mold spread-if-path-or-group 'p/a/t/h
== "p a t h"

How about THAT? All of this hinges on the idea that MOLD doesn't generally know how to mold isotopes, as they have no representation. It just chooses to interpret the request to mold a splice isotope as "contents matter, no delimiters".

More control, more clarity, and the death of another /ONLY. What more could you ask for?

1 Like

On The Theme of ALL-SPLICE!...

Historically I discouraged the idea of "too many functions taking splices".

One good reason would be that SPLICE! carries no binding.

Hence if you were going to do this:

>> reduce ~[1000 + 20 300 + 4]~
== \~[1020 304]~\  ; antiform (splice!)

Then REDUCE would have to assume you wanted the binding from the callsite (the way COMPOSE does by default).

Today's REDUCE uses the binding of the thing you pass it... and I think NOT using the binding of the callsite should be the overwhelming default for functions.

But... COMPOSE is different. It can operate on unbound material (strings, even!) (or: "strings, especially!!!")

So is there any good reason to prohibit it for COMPOSE?

:thinking:

I was looking at this piece of the implementation of the SOURCE function:

    keep opt spread compose [
        return: (conditional ret.spec)
    ]

    for-each [key param] f [
        keep spread compose [
            (decorate param key) (opt param.spec)
                (opt param.text)
        ]
    ]

And I thought "that gets tighter if I could use a SPLICE!"

    keep opt compose ~[
        return: (conditional ret.spec)
    ]~

    for-each [key param] f [
        keep compose ~[
            (decorate param key) (opt param.spec)
                (opt param.text)
        ]~
    ]

Is that so wrong?

I think it makes things more obvious... you can look at it from a few feet away and go "oh yeah, a splice!"

It's also more performant. You duck a function call to SPREAD.

Will People Start Expecting This?

I get a bit worried about people failing to grasp the nuance of why COMPOSE can do it, and REDUCE can't.

And it seems like it could open the floodgates to questions like "should JOIN take splices?"

>> join ~[a b c]~ [1000 + 20 300 + 4]
== \~[a b c 1020 304]~\  ; antiform (splice!)

How about FIND? Does SPLICE! have a position... is it a series?

As useful as these things may seem, I have a sneaking suspicion that they're a bad idea. You start to lose grounding pretty quickly. (I've explained why I don't think QUOTED! or QUASIFORM! etc. type things should be supported by generic operations.)

I Think It's Okay For COMPOSE

COMPOSE may be an odd one out, because it's a templating thing. Something about its nature may just be more suited to "I work in whatever skeleton you gave me."

Certainly I'd use it.

1 Like

These questions start to weigh on the internals a bit...

Anything I try to say is "different" about SPLICE! from BLOCK! has to face the truth that splice conceptually is a block for the exact reason that we don't want to have a different API for splices from the API for blocks. That's the core idea of isotopes.

So when you pull a splice out from the antiform domain to work with it, that block needs to obey all the API functions that a block would obey.

If I pull back here a bit and ask "What would be the harm of SPLICE! being considered a series", it's easier for me to point to the harm to the internals than it is to point to the harm in usage.

The type discrimination in the core distinguishes between antiforms and regular values, but not "splices and everything else". Dealing with antiforms is special and trickier, and you try not to do it unless you can't avoid it. (The Option(T) type is a trick that allows optional cells that actually hold non-antiforms -or- a null to be encoded.)

But from a user's point of view, if you can do a FOR-EACH on a SPLICE! without lifting it... is that harmful?

Looked at from a certain perspective, one might think of it in reverse, that LENGTH OF the BLOCK! [a b c] should be 1, because appending it to a list only appends 1 item, while the SPLICE! |~[a b c]~| should have length 3, because appending it to a list appends 3 items.

:face_with_diagonal_mouth:

I think the risks of being too casual about SPLICE! mirror the risks of being too casual about quoting. You don't want to wind up in a situation where you write generic code that just incidentally accepts a SPLICE! and treats it no differently than a BLOCK!. That would lead to chaotic usages where people just ignore or forget to un-splicify things before passing them around.

So that makes it seem like making things like splice.1 or for-each item splice [...] be errors is the right call for now.

1 Like