Why COMMENT Vanishes, but not EVAL of [COMMENT]?

Functions like COMMENT and ELIDE return VOID!

>> comment "vanishes"
== \~\  ; antiform (void!)

>> 10 + 20 comment "vanishes"
== 30

>> 10 + 20 elide print "Prints but vanishes" 
Prints but vanishes
== 30

>> 10 + 20 (elide print "Grouping keeps vanishing behavior") 
Grouping keeps vanishing behavior
== 30

You can save the result of a vanishing evaluation in a variable:

>> result: comment "make a void"
== \~\  ; antiform (void!)

...and using ^META-fetching, you can copy that into another variable:

>> other: ^result
== \~\  ; antiform (void!)

...aaaand if you EVAL a block of code, it looks like it should vanish... because COMMENT returns void:

>> eval [comment "make a void"]
== \~\  ; antiform (void!)

But it seems that unless you call a function like COMMENT or ELIDE directly, the vanishing gets lost...and it makes an empty PACK!...which doesn't vaporize (calls itself "heavy void").

>> 1 + 2 ^result
== \~()~\  ; antiform (pack!) "heavy void"

>> 1 + 2 eval [comment "test"]
== \~()~\  ; antiform (pack!) "heavy void"

>> f: make frame! comment/
>> f.skipped: "test"

>> 1 + 2 eval f
== \~()~\  ; antiform (pack!) "heavy void"

The same thing seems to happen in PARSE. Rules like ELIDE work fine if you use them directly, and you can put them in a block:

>> parse "aaabbbb" [tally "a" elide some "b"]
== 3

>> parse "aaabbbb" [tally "a" [elide some "b"]]
== 3

But if you abstract the block into a rule variable, the vanishing behavior goes away:

>> rule: [elide some "b"]

>> parse "aaabbbb" [tally "a" rule]
== \~()~\  ; antiform (pack!) "heavy void"

Is there any way to get invisibility if you're not using a direct call to these functions?

You've encountered a safety mechanism... that "busts ghosts"... turning VOID! into HEAVY VOID.

Only A Few Functions Are "VANISHABLE" By Default

There's just too much room for error with vanishing, when you write things like:

x: (<some expression> eval code)

x: (<some expression> ^var)

parse data [... x: [<some expression> rule] ...]

You're using evaluation patterns where you typically expect the last synthesized product to be the overall result of the evaluation. It's disorienting when that product vaporizes too easily.


Surgical Tool: Use the IDENTITY Operator (^)

Asking for the function result "as-is" can be done with the IDENTITY function, which is also available as ^

>> 1 + 2 eval [comment "test"]
== \~()~\  ; antiform (pack!) "heavy void"

>> 1 + 2 identity eval [comment "test"]
== 3

>> 1 + 2 ^ eval [comment "test"]
== 3

>> f: make frame! comment/
>> f.1: "test"

>> 1 + 2 ^ eval f
== 3

>> 1 + 2 ^void
== \~()~\  ; antiform (pack!) "heavy void"

>> 1 + 2 identity ^void
== 3

IDENTITY intervenes and stops the distortion that makes the heavy void result.

But this means that if something is returning heavy void as its actual result (not just a safety intervention from a multi-step evaluation), it won't vanish.

The "Humane" Operator: GHOSTLY

If you don't care about stopping the creation of heavy voids and just want to erase ANY-VOID?--heavy or not--you can use GHOSTLY.

This can be used in places where the influence of IDENTITY would be "too late", such as when a heavy void branch is created:

>> 1 + 2 if 10 = 10 [comment "test"] else [<foo>]
== \~()~\  ; antiform (pack!) "heavy void"

>> 1 + 2 ^ if 10 = 10 [comment "test"] else [<foo>]
== \~()~\  ; antiform (pack!) "heavy void"

It's important to the machinery of ELSE/THEN/ALSO to know if they should run or not. So the actual emerging value out of the failed IF is a light VOID! only if the branch does not run. Otherwise even if the branch runs, it has to be at minimum a heavy void or heavy null to cue the ELSE/THEN/ALSO.

GHOSTLY happily erases the heavy voids that IDENTITY will not:

>> 1 + 2 ghostly if 10 = 10 [comment "test"] else [<foo>]
== 3

IDENTITY Is Available In UPARSE Too

The BLOCK! and GROUP! combinators are "ghostable" by default, but the WORD! combinator is not:

>> rule: [elide some "b"]

>> parse "aaabbbb" [tally "a" ^ rule]
== 3
1 Like

I see. But it seems that if this is what you intended, then it would be very rare that you would actually want the vanishing behavior. You probably want to lift the possibly-VOID! value, and then unlift it:

^x: unlift (<some expression> lift eval code)

But if you don't do that... is it better to silently "corrupt" the VOID! into an empty PACK! vs. give an error?

An error could to guide you to:

  • Switching to a LIFT/UNLIFT
  • Explicitly using a HEAVY conversion function
  • Using the ^ operator?

Another thing regarding invisibility: It seems like there's a discrepancy in the EVAL behavior of BLOCK! and GROUP!. They give the same answers if you use the ^ operator:

>> eval [^ eval [comment "hi"]]
== \~\  ; antiform (void!)

>> eval $(^ eval [comment "hi"])
== \~\  ; antiform (void!)

But if you don't use the ^ operator, it seems GROUP! evaluations are more forgiving... and don't make heavy voids as often:

>> eval [eval [comment "hi"]]
== \~()~\  ; antiform (void!)

>> eval $(eval [comment "hi"])
== \~\  ; antiform (void!)

Is this intentional?

It doesn't seem like an absolute rule, because you do have to use the ^ operator for evaluation steps that aren't at the beginning:

>> eval $(10 + 20 eval [comment "hi"])
== \~()~\  ; antiform (pack!) "heavy void"

>> eval $(10 + 20 ^ eval [comment "hi"])
== \~\  ; antiform (void!)

It's Not "Corruption"... It's The Best Strategy

I've looked at enough cases to know that a VOID! and empty PACK! are interchangeable a lot of the time. This is why they are called "VOID" and "HEAVY VOID"... and both answer truthy to ANY-VOID?. It's par for the course in the isotopic model.

Think about examples like this:

>> plusones: []

>> map-each x [1 2 3 4 5] [append plusones x + 1, if odd? x [x * 10]]
== [10 30 50]

>> plusones
== [2 3 4 5 6]

IF returns a VOID! when it doesn't take its branch. But you wouldn't want that MAP-EACH to give [10 [2 3] 30 [2 3 4 5] 50]. So you see why IF isn't vanishable.

Doing the "distortion" to the heavy void here was very useful, and I think the MAP-EACH result is exactly what you would want.

(Related discussion: "Choosing Between Returning VOID and HEAVY VOID")

YES :white_check_mark: ... EVAL Has :two: Modes Of Operation

  • "transparent" mode - like how GROUP! works, where (expr) and expr behave the same, at the cost of having distinct behavior for all steps producing VOID! up until the first non-VOID! value. (No need to use the ^ operator in those initial steps.)

    • you wouldn't want void? (eval [^ comment "hi"]) to be falsey. It needs the same answer as void? eval [^ comment "hi"] which is truthy.

    • Merely parenthesizing a single expression isn't expected to change what it produces.

  • "regimented" mode - like how eval block is expected to work, where every step has the same "afraid of ghosts" behavior, even before the first non-VOID! value is seen.

    • The consequence of this is that eval [expr] may give a HEAVY VOID answer when plain expr would have given a "light" VOID

    • This makes it trivial to simulate the exact answer of a full EVAL using sequential EVAL:STEP calls, and just throwing away any VOID!s.

These two modes could be controlled by a refinement, rather than being based on the datatype. But that would require coming up with a name...and also wouldn't draw attention to the reason the modes are distinct, and specially fit to their types.

By guiding the behavior by the type, this increases the odds that people will use the right evaluation...or at least think about why the distinction exists.

To further the prescriptiveness to try and guide people to the coherent answer, GROUP! does not support EVAL:STEP. Because if it did, then getting the same answer that a non-stepping evaluation would get would involve switching modes after you saw a step that produced a non-ghost (e.g. stop passing in whatever :UNAFRAID refinement, or in this case switching from passing a group to passing a block).

If you want to evaluate a block with group semantics or vice-versa, use the AS operator:

>> eval as group! [eval [comment "hi"]]
== \~\  ; antiform (void!)

>> eval as block! $(eval [comment "hi"])
== \~()~\  ; antiform (pack!) "heavy void"

:+1:

I see now that this isn’t a “cute representational detail”, it’s one of those load-bearing distinctions where everything else (composability, safety, explainability) quietly depends on getting it right.

If I had to put the core in one sentence:

VOID! is the thing that can disappear without leaving debris.
Empty PACK is the thing that survives transport and proves “something happened”.

That’s the whole reason the system can have both:

  • aggressive elision (comment-like vanishing, opt-out flows, “not taken” branches)
  • and also safety (“did you mean to drop that?”, multi-return, branch detection, ELSE/THEN logic)

Why comment must vanish

comment is a non-semantic intake. The user is explicitly writing “this is not code”. It has to behave like a syntactic eater. So it's a vanishable function:

  • it should be allowed almost anywhere
  • it should not perturb pack arity
  • it should not leave a detectable artifact unless the user insists

So:

  • comment "hi" → VOID! (air)
  • and if you evaluate the word comment (or do something that treats it as data), now you’re explicitly asking to observe the mechanism, so it can’t vanish in the same way.

That’s not inconsistency, it’s the “measurement collapses the wave function” moment: using it as code vs. treating it as data.

Why empty pack must not vanish

This is the crucial dual:

An empty pack is not “nothing”.

It’s the record of a deliberate yield, and it carries with it the right to participate in control flow:

  • tells ELSE/THEN “a branch ran”
  • tells higher levels “don’t treat this like absence”
  • makes “something but nothing” transportable

So empty pack is like a returned envelope with no letter inside: the envelope matters.

Why you can’t unify void! with empty pack

If you tried to make void! just be ~()~ (or ~(void)~), you lose one of two powers:

  1. If VOID! becomes packy, then comments and opt-outs become observable junk—you can’t write them freely without them contaminating results.
  2. If packs become ghosty, then you lose the ability to represent successful-but-empty outcomes and multi-return shape without ambiguity.

So you need both poles:

  • VOID!: a value whose job is to not be a value (inert absence, “air”)
  • EMPTY PACK: a value whose job is to be a value that represents emptiness (structured nothing, “envelope”)

The narrative I’d teach (and defend)

People get hung up because they expect one “void” concept.

But you’ve got two:

  • void as absence (VOID!) — “there isn’t even a trace”
  • void as result (empty pack) — “there is a trace: the act happened”

And that’s exactly what lets you have:

  • vanishing comments
  • safe composition
  • branch logic that doesn’t lie
  • multi-return as first-class

It’s one of the clearest examples of your whole thesis: the states already existed; languages were just encoding them badly.

1 Like