Make Splicing by default only apply when appending a BLOCK!

This is an idea which has been floating around for a while...which I think we should go ahead and do.

We know that APPEND and APPEND/ONLY can be a sticking point with new users, when they splice blocks together. We also know that even experts get bitten by things like:

>> block: copy [a b c]
>> path: 'd/e
>> append block path
== [a b c d e]

I won't rehash all the arguments for why APPEND of a BLOCK! splices by default, instead of being the uncommon case you have to ask for (APPEND/SPLICE). Let's just say that's how it is, and how it has to be.

But the above behavior does not have to be the rule. We already know BLOCK! is special, an IF statement executes it... but it won't execute a PATH! or even a GROUP!. Why can't the splicing be another special magical power of BLOCK!?

>> block: copy [a b c]
>> path: 'd/e
>> append block path
== [a b c d/e]

>> append block as block! path
== [a b c d/e d e]

With the ability to alias one ANY-ARRAY! type as another, without copying, you can turn a splicing situation into non or vice-versa just by making a value cell which re-types the array. This provides plenty of flexibility. And I think that the idea that only the [blocks] do it goes along with the [o] logo: blocks are special.

Does anyone have objections?

I like the second way better. In fact when I want to append a path to a block why need to specify? append/only block path appends the path in R2 but I rather specify when I do want to split/splice.
Do you also consider append to append block values or the entire block by default?
block2: [1 2 3]

append block block2
== [a b c [1 2 3]]

(append block as block! block2 looks weird)
Should keep a bit consistent and change this is a big step to get used to?

I should point out that the "dark side" of such a change is when you have two like-typed things and you want to splice them together.

>> path: copy 'a/b
>> append path 'c/d/e
== a/b/c/d/e

To a casual observer, this seems more reasonable than producing a 3-element path whose 3rd element is c/d/e. Especially because there's no good way to mold a path-in-a-path. (In fact, it's totally broken today.)

In Rebol2/R3-Alpha/Red all say this:

>> path: copy 'a/b
== a/b

>> append/only path 'c/d/e
== a/b/c/d/e

>> mold/all path
== "a/b/c/d/e"

>> length? path
== 3

Paths-in-paths need a better answer, because they will be possible to make. Here I muse about encodings, and propose a "weird" way of doing it, but ask if anyone has a more intuitive-looking scheme:

I did once propose that splicing only happen when the two types were the same, but this creates even more combinatorics, making things seem random. A dirt-simple rule (blocks splice without /ONLY, nothing else does) serves better in the long run.

1 Like

Being bitten by the spliced path just now, I fully support only splicing blocks.
Maybe there could be some 'splice function which would splice everything?

1 Like

@IngoHohmann There is a new invariant in play that I have proposed. The idea is that appending to a target type from a value of a source type--when the types do not match--will be equivalent to running a TO conversion of the source value to the target type, and then doing the append.

Thus to not have the splicing behavior, this means that TO BLOCK! of a PATH! would put the PATH! in a block.

 >> to block! 'a/b/c
 == [a/b/c]

I think that with AS BLOCK! and COPY AS BLOCK! available, this interpretation isn't a bad one. Those who need to reinterpret a path in a blocky way can do it--possibly more efficiently than a traditional TO if they don't need the COPY.

Though carrying these rules through to consistent conclusions does seem to be leading toward one of my proposals from some time ago--that you get the splicing only when types match:

>> append copy 'a/b/c quote (d e f)
== 'a/b/c/(d e f)

>> append copy quote (a b c) [d e f]
== (a b c [d e f])

>> append copy quote (a b c) as group! [d e f]
== (a b c d e f)

e.g. it may be sanest for to block! quote (a b c) => [(a b c)], so it seems that to group! [a b c] => ([a b c]) would line up with that. I think the existence of AS makes this more viable.

So there's sort of good news about this one: immutable paths, mean they're no longer in the category of ANY-ARRAY!, and aren't subject to the ANY-ARRAY! splicing rules. Furthermore, when it comes to appending to a PATH!, we no longer have to worry about whether appending to them splices or not...because you can't append to them...at all! :stuck_out_tongue:

But you will be able to JOIN paths...and JOIN of two paths will clearly do some kind of splice (exactly what kind of splice it does related to blanks on the head and tail remains to be seen, as NewPath emerges).

That leaves us with really the one question about GROUP!. Does it splice by default or not?

 >> block: copy [a b c]
 >> append block '(d e f)
 == [a b c d e f]

@giuliolunati has suggested that he might like GROUP!s to not splice in COMPOSE by default. But I have made an argument from consistency that if it splices there, so it should in COMPOSE also:

 >> compose [a b (first [(c d e)]) f]
== [a b c d e f]

It seems to me that we're as resolved on this as we're going to get, and that we just accept that GROUP!s, as ANY-ARRAY!, splice by default along with blocks. Pinning down Beta/One means only breaking status quo when there's a clear motivation and "right" answer.

Well these days to splice or not to splice could be controlled with quoted-, get-, set- variants.

This could also be used in paths regarding blanks. My gut feeling is that by default blanks should not be kept.

I've considered whether there's any way to exploit quotes for this.

It made sense to make branches be soft-quoted, so that IF could see the quote mark at the callsite. But we can't make the value to APPEND soft quoted ("escapable literal")--you don't want to always be putting GROUP!s around the appended thing.

So exploiting it would mean you'd be "new-QUOTE"-ing the argument, e.g. adding a quote level to its evaluated argument. e.g.:

append block uneval [a b c]  ; uneval will be renamed to QUOTE, one day

But you don't want to have to quote strings to append them. Hence you get in some amount of trouble writing a generic append:

append block uneval foo

Let's say foo might be a TEXT! or it might be a BLOCK!. You just quoted it. The proposal here would be that a quote-level-1 block gets knocked down to quote-level-0 and appended. But does APPEND treat quote-level 1 text the same as quote-level-0 text?

I'd certainly be interested if you tried working through the details with a prototype that exploited quoting for splice instructions, but I don't know if I see a way to do it that's going to be better than status quo.

I haven't had time for it, but I think I see what you mean.

I mentioned how quoting doesn't really work due to the second parameter of APPEND being evaluative (hence it would strip one quote level off...)

But with @word, @some/path, @[...], @(...), they are inert in the evaluator. This actually could work if we wanted it to.

 >> b: [1 0 2 0]
 >> j: [3 0 4]

 >> append b j
 == [1 0 2 0 3 0 4]

 >> append b @j
 == [1 0 2 0 [3 0 4]]

 >> append b @[3 0 4]
 == [1 0 2 0 [3 0 4]]

 >> append b @(reverse [4 0 3])
 == [1 0 2 0 [3 0 4]]

But if it cannot distinguish a "literal" @ at the callsite from an evaluative one, you run into problems like:

for-each item [a b: @c] [
    append block item
]

It would be quite counter-intuitive, in my view, for that to be looking up a variable c and appending its contents as-is into the block. :-/ That's a level beyond the existing frequent-counter-intuitiveness of splicing blocks when /only is not specified.

But if we distinguished at the callsite between @ forms and otherwise normally evaluated the parameter to append, that might be interesting:

 >> block: [a b c]
 >> append block [d e f]
 == [a b c d e f]

 >> block: [a b c]
 >> append block @[d e f]
 == [a b c [d e f]]

 >> var: @[d e f]

 >> block: [a b c]
 >> append block var
 == [a b c d e f]

 >> block: [a b c]
 >> append block @var
 == [a b c @[d e f]]

This would make names like LIT-BLOCK! even more salient, as you're saying "do something literally". (in fact, APPEND/ONLY could have been APPEND/LIT and perhaps made more sense.)

It would require APPEND to have a skippable parameter that would be mutually exclusive with a normal parameter. If the skippable parameter was present, it would not go on to the next argument.

The alternative would be to codify a bit which indicated if something was literal at the callsite or not. I don't like that as much, because it hampers clear use of APPLY. Having two separate parameters allows an APPLY to pick which one it sets based on its goal.

This is a cool thought experiment about applications of @, which is a tool which we are learning about as we go. One problem with it is generalization, e.g. how does this relate to MAP-EACH processing its result and knowing whether to splice that or not. Bizarre callsite <skip> conventions won't help there... it has to be controlled either by a property of the value itself or a refinement.

Interesting to keep thinking about...

I tried out the concept of changing Rebol's historical behavior of splicing by default, to only splicing if you say /SPLICE. It incorporated the concept of modal parameters, so append [a b c] @[d e] would be a way of saying "splice".

And I didn't like it.

Here's an extraction of why, that was on another thread but I'm moving it here to make the useful part of that post more useful.


Consider this case from the tests...It's code that builds code with COLLECT and KEEP COMPOSE. I'd previously been pleased with tricks like how compose [(...): ...] could produce the SET- forms of the composed material (if there was one), and can even do better with automatic word conversion (e.g. why should compose [(unspaced ["meth" n]): ...] be an error just because there's no SET-TEXT! form of string, it could just be an unbound word):

keep compose [
     (to word! unspaced ["meth-" n]): method [] (collect [
            keep 'var-1
            repeat i n - 1 [
                keep compose [
                    + (to word! unspaced ["var-" i + 1])
                ]
            ]
         ])
    ]

So I'm of course disappointed to imagine having to change those to either KEEP/SPLICE -or- KEEP @(...). The parentheses junk it up...when you're using a COMPOSE you really want to see the parentheses in the compose itself primarily:

keep @(compose [
     (to word! unspaced ["meth-" n]): method [] (collect [
            keep 'var-1
            repeat i n - 1 [
                keep @(compose [
                    + (to word! unspaced ["var-" i + 1])
                ])
            ]
         ])
    ])

So that's depressing. KEEP/SPLICE isn't as bad here (KEEP/MANY? KEEP/MULTI? Whatever you call it):

keep/splice compose [
     (to word! unspaced ["meth-" n]): method [] (collect [
            keep 'var-1
            repeat i n - 1 [
                keep/splice compose [
                    + (to word! unspaced ["var-" i + 1])
                ]
            ]
         ])
    ]

But is this a tradeoff to be concerned about?

It's tough to really think about all the damage past and present that implicit splicing has done.
Not to mention all the /ONLYs that are out there (plus the ones forgotten), when just looking at how one previously "slick" seeming example has gotten less palatable.

Is KEEP/SPLICE really so bad here? If it really gets on your nerves in a larger sample, why not define an emitter that does both the compose and the splice and use that?

 collect [
     emit: adapt :keep/splice [value: compose value]

     emit [(1 + 2) (3 + 4)]
     emit [(5 + 6) (7 + 8)]
 ]
 == [3 7 11 15]

Or maybe whether KEEP splices is directed by the code block, in the same way as MAP-EACH...

>> collect [
    keep compose [(1 + 2) (3 + 4)]
    keep compose [(3 + 4) (5 + 6)]
]
== [[3 7] [11 15]]

>> collect @[
    keep compose [(1 + 2) (3 + 4)]
    keep compose [(3 + 4) (5 + 6)]
]
== [3 7 11 15]

Then if you know you have some blocks that aren't meant to be spliced, you put them in a block to protect them.

Big Picture Thought: Reliability Of Parts is the Key

My feeling is that Rebol plateaued because it gave some basic manipulation abilities that seemed to work well enough. But when people tried to do bigger things it let them down. You get a cool idea like wanting to write your own loop construct, but find suddenly RETURN didn't mean what it needed to.

So lamenting the fact that you have to say append/splice compose where once you could just say append compose is probably the wrong granularity to be looking at. Systemically the question is whether the system is versatile enough to be customized in notable ways. And one of those ways will be Redbol with splice by default and the old /ONLY behavior just as it was.

But on the good news side, the system does flex as intended to add features to be able to try them.