Putting Splicing Intent On APPEND'ed Value

Editor's Note: This thread is a heavily curated edit of posts regarding considerations about getting rid of the /ONLY refinement. It tries to pick out notable steps in reaching the current conclusion, so the reasoning can be followed by future readers.


Forgive me if this was mentioned, but if single-element blocks are more efficient than refinements, you could have a function that wrapped a value in a block (like BLOCKIFY, but optimized for single element block and equivalent to what BLOCKIFY/ONLY would do). This would be a little like '[quoting] but could be applied to a word:

; calling it ONLY 😈
>> append [a b c] only block: [d e f]
== [a b c [d e f]]

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

Correct me if I'm wrong, but this proposed ONLY function would simply create a single value cell with the block reference, which would seem pretty efficient.

It'd be very easy to shim:

only: func [value][
    reduce [value]
]

I'm still fond of ENVELOP over BLOCKIFY as a name. I don't think ONLY would make the cut. Naming is tricky as it is sort of a hack—it's purpose is to make a block a singular value but in actuality it is creating a new value of which the old one just happens to be the only content.

I like this direction. Compared with append/only [a b c] block:

  • You lose a slash, cutting down on visual noise (and it's cheaper at source level by virtue of not needing a series node for the path, just one more cell in the parent array).

  • It can be cheap at the runtime level as well if the single-cell-block optimization works. (We can also make calling ONLY extremely fast...there are tricks for single arity functions like this that can bypass function invocation mechanics entirely, though debug modes should be taken into account to suppress such optimizations.)

  • The concept of ONLY as a modifier to the item--instead of a "different kind of append"--gives APPEND a more uniform behavior.

  • In the flow of words it feels better to place the intent not to splice on the item.

2 Likes

A quick enumeration of /ONLY functions crudely gathered:

sort collect [
    for-each word words-of lib [
        all [
            action? get/any word
            find spec-of get word /only
            keep word
        ]
    ]
]

Using as the shim:

only: func [val][compose [(val)]]

Against the current iteration of ReplPad:

>> system/commit
== "646d3c4ec896f3bde06043b2535ab86d8981c4c7"

append, repend

>> append [a b c] only [d e f]
== [a b c [d e f]]

bind

Not applicable

change

>> head change next [a b c] only [d e f]
== [a [d e f] c]

clean-path

Not applicable

compose

Seemingly ONLY is currently the default

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

construct

Not applicable

do

Not applicable

find, find-last, find-reverse

>> find [a b c d e f [d e f]] only [d e f]
== [[d e f]]

info?

Not applicable

insert

>> head insert [a b c] only [d e f]
== [[d e f] a b c]

math

Not applicable (also doesn't appear to be working)

mold, remold

Discussed elsewhere.

>> mold only [foo]
== "[[foo]]"

random

Not really sure if this is something that would apply.

resolve

Not exactly sure what this function does.

select

>> select [a b c d e f [a b c] [d e f]] only [a b c]
== [d e f]

One that wasn't picked up by LIB scouring:

keep

>> collect [keep [a b c] keep only [d e f]]
== [a b c [d e f]]

Over half of these should be a win

So I want to get things sync'd on this proposal, because I think all the degrees of freedom are now known.

  • The "bad news" is that my single-block optimization will not work out.

    • Single-element blocks are still relatively cheap (e.g. notably cheaper than a 2-element block), but not cheap-as-free.
  • The "good news" is that quoting is now cheaper than ever...at up to 254 levels of quoting. :mount_fuji:

    • It really is cheap-as-free, bumps a byte on a cell (so the "trick" was just about finding ways to make that byte available, while allowing everything else that wants flags on cells to work some other way)

All we have to do is to say that QUOTED! things that append drop a level of quoting, and QUOTE becomes the magic operator we've been looking for:

 >> append [a b c] [d e]
 == [a b c d e]

 >> append [a b c] quote [d e]
 == [a b c [d e]]

That looks pretty slick to me, and if you're willing to embrace the new ^META operators you get some succinct and speedy behaviors for experts:

>> append [a b c] ^[d e]
== [a b c [d e]]

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

etc. However...

In a nutshell, no matter what proposal we have put forward for dealing with this mire, I have been unable to shake concern about people who are working with code under the illusion of an invariant... and that invariant does not hold.

>> block: [#a {b} [c d e] %f]

>> pick block 2
== {b}

>> find block pick block 2
== [{b} [c d e] %f]

>> index of find block pick block 2
== 2

If you pick a (unique) thing out of a block, and then find that thing in the block, the index will be where you picked from.

But then you get a non-atomic default out of FIND.

>> block: [#a {b} [c d e] %f]

>> pick block 3
== [c d e]

>> find block pick block 3
; null

In terms of a poster child for "this still seems problematic", I actually kind of feel FIND is a bit worse than APPEND for some reason.

The thing is that you're usually looking at a line of code like find blarg pick mumble index and you have very little to go on. I make mistakes with this stuff constantly.

Our change to LOAD to always return a BLOCK! feels like it tightened things up, and LOAD-VALUE seemed to help. I guess I'm just wondering about the real value in the "collection-or-item" polymorphism. Any additional thoughts on the exploding brain thread appreciated.

The issue I have with doing the opposite of ONLY—let's call it SPREAD—is what is the interim value?

>> block: [a b c [a b c]]

>> pick block 4
== [a b c]

>> find block pick block 4
[[a b c]]

>> find block spread pick block 4
[a b c [a b c]]

>> spread pick block 4
???

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

It would seem to have virtue over ONLY and is a better word.

1 Like

An excellent question... and a perfect one for the isotopic/atomic era. :atom_symbol:

(I've called this SPLICE, which I prefer... although really something like CONTENT OF is probably more generic when applied to things like FIND or PARSE. find [a b c d] content of [d e].)

We could have isotopic blocks, and call them "splices". :astonished: Functions that took normal parameters would raise errors if you tried to pass them one, but ^META arguments would be able to process them (received as plain BLOCK! vs. a quoted BLOCK!)...and we'd make APPEND and friends take their arguments as meta.

Isotopic blocks don't exist today (though they'd be trivial to add, they're just BLOCK! with a quoting level of -1). But as an approximation of them I can paste into ReplPad right now, we can slip the signal into "isotopic errors", e.g. definitional "failures"... which only some functions would be receptive to:

spread: func [x] [
    return fail make error! [  ; "definitional" FAIL
        message: "uncaught spread"
        id: 'spread-signal
        arg1: :x
   ]
]

append: func [series ^value [<fail> <opt> any-value!]] [
    either error? value [  ; a failure (normal ERROR! to ^value would be QUOTED!)
        if value.id != 'spread-signal [
            fail value   ; "non-definitional" FAIL, raises error normally
        ]
        value: value.arg1  ; argument to spread, not quoted
    ][
        ; leave quoted otherwise (non-isotopic values get quoted by ^ on value)
    ]
    return lib.append series value  ; quoted values appended "as-is"
]

The effect might seem like you could have done this in historical Redbol with TRY/EXCEPT...

>> append [a b c] [d e]
== [a b c [d e]]

>> append [a b c] spread [d e]
== [a b c d e]

But no, it's a direct relationship via the output piped to a ^META/<fail> argument. You don't get undesirable effects from arbitrarily deep stacks:

>> append [a b c] reduce [spread [d e] 1 + 2]
** Error: uncaught spread

And that, kids, is why definitional failures are so awesome! :stars:

(I'll add that definitional failures do not require triggering a longjmp or C++ exception, though you do incur overhead from creating the ERROR! object, of course.)

Anyway there's a proof of concept. But isotopic block splices would be much more efficient, and make significantly more sense.

It would seem to have virtue over ONLY and is a better word.

So my perspective was at one time informed from having tried a lot of approaches...and I've gone through so many times of re-rigging the entire code corpus it's sort of maddening.

When I did, I was surprised to find that splicing as default intent was a rhythm I preferred. It's been a while since I've done it...and I probably should have taken better notes on exact cases that made me feel that way.


(UPDATE: going through with the new proposal showed me that the cases that feel the biggest bummer to change are with literal blocks, e.g. append file-list [%foo.txt %bar.txt]. There's a bit of a negative feeling when having to change that to APPEND/SPLICE. I think this negativity is less when it's not a refinement, so append file-list splice [%foo.txt %bar.txt] ... and maybe we could come up with a symbol for this. But having looked at this so long I think it's just right... and if you want to make that cleaner you can make a gathering construct local to the function called ADD (or whatever) that specializes it all out to just add [%foo.txt %bar.txt])


Maybe my feelings are outdated and I should reevaluate it in light of these isotopic blocks. I think so. It's a systemic solution that would apply across the board (e.g. in COMPOSE).

splice: func [value [any-block! any-group! any-path! any-tuple!]] [
    return unmeta as block! value  ; isotopic, e.g. "quoting level -1"
]

Maybe things need to be controlled by refinements... like which things you want to splice or not. Maybe it's an arity-2 function... splice block! value, splice [block! any-path!] value. There could be variations...build your own...use symbols. Maybe skippable @ parameters... like splice value vs. splice @block! value or splice @[block! any-path!] value

(Point is it's something you could write in usermode, and design the behaviors creatively... functions that sometimes return a block isotope, and sometimes don't.)

I'm warming up to the idea by realizing that we basically are already having to make APPEND & friends act like they take value as ^META anyway...so...might as well make it official.

Either way, no use rushing at this point! :snail: Better to get it right.

2 Likes

Something I've lamented is that we don't have an UNMETA glyph on the keyboard. I want down arrow.

foo: func [...] [... return unmeta value]
bar: func [...] [... return ↓value]

Then we could have DOWN-WORD!. But DOWN could just be a general operator for UNMETA:

>> append [a b c] reduce [1 + 2 3 + 4]
== [a b c [3 7]]

>> append [a b c] unmeta reduce [1 + 2 3 + 4]
== [a b c [3 7]]

>> append [a b c] ↓ reduce [1 + 2 3 + 4]
== [a b c 3 7]

This would leave SPLICE open for cases that were more parameterized, maybe arity-2 cases.

Given that we do support UTF-8, maybe going ahead and investing in a key binding for up arrow as synonym for META and the down arrow as a synonym for UNMETA is worth it...

1 Like