Sending Values into a BLACKHOLE!

Multiple return values have an interesting angle to them: the more values you request... the functionality you ask for might wind up being different.

Consider TRANSCODE. If you don't use multi-returns...or just ask for one get the whole thing transcoded as a block:

>> transcode "<abc> <def>"  ; plain behavior
== [<abc> <def>]

>> block: transcode "<abc> <def>"  ; set-word! (follows usual rules)
== [<abc> <def>]

>> [block]: transcode "<abc> <def>"  ; same behavior as `block:`, enforced
== [<abc> <def>]

But if you supply it with a parameter in the second slot, it assumes you want /NEXT and will set that argument to the next position.

>> [value pos]: transcode "<abc> <def>"
== <abc>

>> pos
== " <def>"

>> value
== <abc>

BLANK! gives you the ability to opt out of slots in the multiple return values that you don't want. But opting out doesn't just keep you from having to name a variable, it also un-requests the behavior...

>> [value _]: transcode "<abc> <def>"
== [<abc> <def>]  ; not a /NEXT, it was "revoked"

What if you want to request the behavior, but not have to bother with creating a variable to store it in? Well now that we are thinking # will be a shorthand for the immutable and not-appendable-to-strings "codepoint 0", it's a perfect choice:

>> [value #]: transcode "<abc> <def>"
== <abc>  ; only the /NEXT item

The value was requested but disappeared...into a black hole. In fact, BLACKHOLE! is now a datatype you can put in a spec to mean "empty issue that is precisely #. It's not validated yet (it's a synonym for ISSUE!...we'll need type constraints before it can work). But it can help keep track of places that use this idea. For instance, SET:

>> set # 10
== 10

The truthiness of # helps write code that distinguishes the wish to opt-out of a behavior from the wish to opt-out of getting its result. Having SET error on blank helps you avoid calculations that may be it works out perfectly:

 do-something: func [
     return: [...]
     secondary: [blank! word! path! blackhole!]

     <local> result
     main-result: process input
     if secondary [  ; unlike BLANK!, empty issue is truthy so branch runs
         result: process/more input
         set secondary result  ; blackhole SET is no-op (BLANK! would error)



With BLACKHOLE! and Isotopes, we have a new excellent choice for opting out of mutating operations...!

Historically, BrianH and others (including me) believed that mutating operations should not let you "opt out" of the target for the mutation. There was too much potential for confusion if you wrote append block [1 2 3] and it silently didn't append, because block was a NONE!.

So we have this:

r3-alpha>> block: none
r3-alpha>> append block [1 2 3]
** Script error: append does not allow none! for its series argument

red>> block: none
red>> append block [1 2 3]
*** Script Error: append does not allow none! for its series argument

Operations without side effects were deemed to be harmless by comparison:

r3-alpha>> block: none
r3-alpha>> select block 'a
== none

red>> block: none
red>> select block 'a
== none

But even that had a kind of nasty property. You could write a long chain of non-mutating operations, and at the end of the pipe you could get a NONE! without knowing where in that pipe the error occurred. (All of this compounded on the fact that a NONE! could have even literally been something that was selected, so your opt-out value could have been an actual selection result!) This gave rise to Ren-C's BLANK!-in, NULL out philosophy.

New Option: BLACKHOLE! in, Isotope Out

I've already shown how useful BLACKHOLE! is with "opted-in" multiple returns and SET. But now, I've made it possible to give blackholes as the input series for operations like APPEND:

>> append # [a b c]
== ~blackhole~  ; isotope

This keeps you from having to write annoying patterns like:

if not blank? my-series [
    append my-series [a b c]

The reason it's different is because blackholes are a very conscious way of opting-in to something without saying where you want the content to go. And they're truthy, so they are a more natural fit for control opposed to blanks.

if my-series [  ; can be a series or blackhole
     append my-series calculation  ; want calculation even if not the append

By comparison, blanks and nulls are sort of "unintentional"...they are very close to being an uninitialized state (or at least a state that is set without thinking too much about it).

Interim Syntax paralleling <blank>

If you want a function that does BLACKHOLE!-in-Isotope-Out, just label the argument with <blackhole>.

>> add-period: func [x [<blackhole> text!]] [
      print "Running ADD-PERIOD"
      append x "."

>> add-period "Hello World"
== "Hello World."

>> add-period # 
== ~blackhole~  ; isotope

So note that although the APPEND would have opted out here, the entire function is skipped if you pass a # to the <blackhole> argument. (APPEND itself uses this same annotation.)

As for whether ~blackhole~ isotopes should casually decay back into # the way ~null~ isotopes decay into NULL... I don't know. I'm trying it out to see how I like it!

1 Like