Non-Interstitial Invisibles: More Trouble Than They're Worth?

Invisibles are a novel and important concept. But they introduce a lot of wrinkles.

While there's a lot of technical complexity--I'm not terribly worried about that (technical problems have technical solutions). But usability and semantic problems need to be confronted.

Early on, I wanted to avoid situations where an invisible could be in a position where it might look like it was an assignment:

x: elide print "It's confusing when X gets [a]"
append [] 'a

Since the ELIDE vanished, the result of the APPEND is what X gets. Confusing!

So at first, I figured it should be illegal to do that. But I later backed off on ruling it out when I thought about a vanishing BREAKPOINT:

x: breakpoint append [] 'a

Why shouldn't you be able to put a breakpoint wherever you want, I wondered? So it became legal.

Now I'm having serious second thoughts about the lenience. I don't know this tradeoff is ultimately worth it; it becomes very slippery when you are dealing with constructs that you aren't necessarily noticing are invisible. This problem feels like it gets worse in PARSE, if plain GROUP!s are invisible and people accidentally put them in slots where they think they are the value, but the value actually comes later:

>> uparse [1 <whoops>] [return collect [
       keep integer! keep ("that was a number!") tag!
   ]]
== [1 <whoops>]

The person thought they were keeping a string literal in their rule. But plain GROUP! is invisible, so they actually kept the rule after the string. (They actually wanted keep @("that was a number!"))

Anyway, these situations are very much parallel. So I'm thinking that the error on invisibility for non-interstitials (e.g. assignment sources or argument slots) may make sense...to say that invisibility is something that can only happen in interstitial locations.

You can still get your breakpoint at odd positions, just introduce a GROUP! so it's not picked up:

x: (breakpoint append [] 'a)

Of course, a group might interfere with something like a COMPOSE, so you might need to use DO or be otherwise creative:

x: do [breakpoint append [] 'a]

We might lament the need to worry more about restructuring the code to accommodate the breakpoint in ways that could disrupt the code. But with dialecting, the appearance of the word BREAKPOINT even just in itself could have caused a disruption. It's the cost of doing business in this paradigm.

What might this imply for other cases, like do [] ?

It may suggest that do [] can be invisible and vanish... just not have the result used in assignments. Which might be perfect; if you get frustrated with that, you can always consciously put a value in-between.

So instead of:

x: thing-that-might-be-invisible

You could say:

 x: (<default-value> thing-that-may-be-invisible)

There needs to be some way of specifically evaluating something and finding out if it was invisible...much like there needs to be a way of evaluating something and knowing if it threw, or raised an error, etc. Not everything fits cleanly as a "result value". But this shows there'd be at least one way of dealing with it besides that.

2 Likes

Well, this neglects at least one important case today:

if let pos: find series item [...]

Currently, LET followed by a SET-WORD! or a SET-BLOCK! adds a binding and vanishes, leaving the existing code to run as it would. This helps make sure it doesn't disrupt any special processing, e.g. looking backwards at the SET-WORD! (think x: me + 1, which we want to work the same as let x: me + 1, hence LET should be staying out of the assignment logic done by ME by quoting backwards).

If followed by a WORD!, LET does something different, it consumes the word and evalutes to that WORD!

if let x [...]

That would act something like:

if 'x [...]  ; the X is a new binding, with that binding visible to ensuing code

It's useful when you want to pass a new variable in as a parameter to something. Not particularly useful in this case, but that's what it would be.

This demonstrates what I've called "opportunistic invisibility"... which offers a MACRO-like ability to expand to some content or no content.

So anyway... not so simple. Maybe this suggests that the act of vanishing is distinct from the act of delegating/forwarding to another expression, e.g. a MACRO?

e.g. maybe LET is legal in a condition argument slot because it's saying something different from "I vanish". It's saying "I am twisting the evaluation feed and supplying new content to take the place of the expression that belongs here".

Hence how LET with a SET-WORD! works is not invisibility...rather that it actually binds and evaluates one expression forward, and then puts in its place the result of quoting the result of that expression.

Another Question: Should Macros Replace Invisibles?

Invisible functions are hard to understand, whereas with generic quoting it can be pretty efficient to make macros that splice quoted material.

m: macro [invisible [logic!]] [
    if invisible [return []]
    return ['invisible]
]

>> 10 m false
== invisible

>> 10 m true
== 10

(Note: there's some bug on this at present, but it's supposed to work.)

Given that invisibility causes so many technical problems, might it be better to have a separate MACRO! type that encodes the capability of being invisible at all (just splice an empty block)?

Then we could say it's invisible macros that are prohibited in non-interstitial slots; and LET would be a macro that's never actually be invisible, e.g.:

if let pos: find series item [...]

=> splices [pos: find series item] as macro result

if pos: find series item [...]

One has to look at the concrete scenarios to see how much of this is just pushing terminology around and what breaks. If there's a new datatype called MACRO! with distinct properties from ACTION! you start having to pay a mental tax of which you're working with... is it legal to DO a macro in isolation, etc.

Maybe I'm worrying about too much and we just have to accept the potential failures like:

>> uparse [1 <whoops>] [return collect [
       keep integer! keep ("that was a number!") tag!
   ]]
== [1 <whoops>]

But I want to point these things out so whatever decisions are being made are conscious ones.

1 Like

I could live with breakpoint being the only special invisible, that can be put everywhere, but I'm not sure whether it's worth the hassle.

Thinking on this particular issue made me realize that the way things work now, plain GROUP! being able to resolve to a value would be fine.

When a rule "sees" the product of a group, it's seeing it as the rule's value-bearing product, not as an inlined rule. This is a completely new concept that works with block rules being value-bearing of their last value-bearing rule product...parallel to DO.

So at least this one pesky instance that had gotten into my head as a new class of problem isn't as pesky. You get:

>> uparse [1 <whoops>] [return collect [
       keep integer! keep ("that was a number!") tag!
   ]]
== [1 "that was a number!"]

I'll keep thinking on it for other potential problem cases.

2 Likes