A Lot To UNPACK: (Replacing the SET of REDUCE BLOCK! Idiom)

We've fretted a lot about the result of REDUCE when an expression produces something that can't be put in a block. At first this was just NULL. But now it's all BAD-WORD! isotope forms.

One reason to fret is the historical idiom of setting multiple values in a block. This has been done with a SET of a REDUCE. Something along the lines of:

>> x: 3 + 4
== 7

>> set [a b] reduce [10 + 20 x]
== [30 7]

>> a
== 30

>> b
== 7

It's your choice to reduce the values or not. If you don't involve REDUCE then the mechanics would all work out. But once you get NULLs and isotopes, the reduced block can't store the values to convey them to the SET...

But What If A Single Operation Did Both...?

Let's imagine we have instead something called UNPACK that by default reduces. Imagine it quotes a SET-BLOCK! on its left.

>> x: 3 + 4
== 7

>> [a b]: unpack [10 + 20 x]
== 30

>> a
== 30

>> b
== 7

UNPACK manages the evaluation one expression at a time, instead of using REDUCE. So as it goes it can set the variables to NULL or isotopes. And by following the multi-return convention of returning the first value, you avoid ever needing to synthesize a block aggregating all the results together.

>> [a b]: unpack case [
       1 = 1 [
           print "This is pretty slick..."
           [select [a 10] 'b, 1 + 2]
       ]
    ] else [
        print "This won't run because the ELSE gets a BLOCK!"
        print "Which is what you want, because the ELSE should be"
        print "what runs if no CASE was matched and have the option"
        print "of providing the block to UNPACK"
    ]
 This is pretty slick...
 ; null

>> a
; null

>> b
== 3

Quoted BLOCK! can Even Avoid A REDUCE

If you already have a block in reduced or literal form, how would you tell the UNPACK about that? It could be a refinement like UNPACK/ONLY. BUT...what if we let quoted blocks signal it?

>> [a b]: unpack the '[1 +]
== 1

>> a
== 1

>> b
== +

Remember that there's more than one way to get a quoted block. I used THE as a literalizing operator (since '[1 +] would lose its quoting level if evaluated). But I could have also used the ^ forms or JUST and gotten the same thing:

[a b]: unpack ^[1 +]

[a b]: unpack just [1 +]

A real aspect of power in this approach is the ability to mix and match. For instance you could have some branches in a CASE which have already reduced data and others that don't, and they could all participate with the same UNPACK operation.

[op1 op2]: unpack case [
    ... [
        print "This branch uses values as-is"
        ^[+ -]
    ]
    ... [
       print "This branch needs evaluation"
       [operators.1, pick [- /] op-num]
   ]
]

Cool Dialecting Twists

It seems to me nice to safeguard that you're not throwing away values:

>> [a b]: unpack [1 2 3]
** Error: Too many values for vars in UNPACK (use ... if on purpose)

As the error says, though, we could indicate we're fine with this through a special syntax:

>> [a b ...]: unpack [1 2 3]
== 1

>> a
== 1

>> b
== 2

(It's not as sketchy when you have too few values, because you can set the extra variables to unset...which will presumably trigger awareness of a problem at some point.)

I think the idea of "circling" values to say which is the one you want the overall expression to evaluate to is a neat idea

>> [a @b]: unpack [1 2]
== 2

But that will have to wait until I can rig back up the @ forms.

And For Show And Tell...

How hard is it to write such a thing, you ask? In Ren-C it's super easy, barely an inconvenience:

unpack: enfixed func [
    'vars [set-block!]
    block [block! quoted!]
][
    let result': ~unset~
    reduce-each val block [
        if vars.1 = '... [continue]  ; ignore rest, but keep reducing
        if tail? vars [fail "Too many values in UNPACK (use ...)"]
        if not blank? vars.1 [
            set vars.1 unmeta ^val
            if unset? the result' [result': ^val]
        ]
        vars: my next
    ]
    if vars.1 = '... [
        if not last? vars [fail "... only at the tail of UNPACK vars"]
    ] else [
        for-each var vars [  ; if not enough values for variables, unset
            if not blank? var [unset var]
        ]
    ]
    return unmeta result'
]

If the ^ and UNMETA seem confusing, the only thing you need to think about is that the META protocol helps you out when you're trying to deal with a situation of storing a value that can be anything...and you need to differentiate a state. I'm making the result "meta" so that I can use plain unset to signal that it hasn't been assigned yet. I could make a separate boolean variable instead, but then I'd have another variable and I'd have to GET/ANY the result...

I'm sure people will start getting the hang of it! :slight_smile:

3 Likes

Very satisfying! I frequently slice and dice values to obtain the parts*, so this is a convenient method to create templates to separate the parts of a block you want to keep.

*E.g., taking a full filename and path (or url!) and split it into: path, filename, suffix; or a date! value and splitting into its component parts.

You can also skip out on values you aren't interested in:

>> [_ b]: unpack [1 + 2 3 + 4]
== 7

>> b
== 7

I think this is going to replace SET of a BLOCK! entirely. But that means it's going to need to let you provide the block as a variable.

(block): unpack [...]

And I think we need another function to handle the case of assigning the same value to all items.

>> [a b]: assign [<some> <block>]
== [<some> <block>]

>> a
== [<some> <block>]

>> b
== [<some> <block>]

(Note: this can't just be [a b]: [<some> <block>] because the multiple return convention creates contention with [a b]: function-returning-a-block ... however, we could make ASSIGN the behavior when it so happens that a literal value is used on the right of a SET-BLOCK! and that would be convenient.)

Getting these behaviors out of SET will be great. Because it tries to put too much into a primitive function. There's too much behavior...like trying to act more like UNPACK when you set a block to a block, and trying to act like ASSIGN when you set a block to a non-block. But then it needs an /ONLY or /SINGLE refinement when you're trying to actually set each variable to the same block.

This makes SET harder to understand when it already has enough concerns.

2 Likes

Do 'a and 'b refer to the same block or are they copies?
I am interested in simple ways of creating and managing lists of synonyms, which is why I ask.

Historically SET would not do a COPY, and I wasn't thinking this would either.

We might try to find a syntax or refinement in ASSIGN for expressing that. But my feeling is that may be beyond the scope of this construct. You might need to just use a FOR-EACH, where it becomes more obvious what you are doing...

for-each var [a b] [set var copy [<some> <block>]]
2 Likes

Now that THE-BLOCK! is in the mix, QUOTED! block no longer seems the best choice here.

It's technically true that you can get a quoted block via META-BLOCK!:

>> [a b]: unpack ^[1 +]

But that makes UNPACK's type-signature take QUOTED!. And a quoted could be anything...so it has to pare that down to see if it's a type it accepts. Plus the behavior takes a bit for your head to get around:

^[1 +] =evaluator=> '[1 +]

Whereas if you consider @[1 +], it is dumber and simpler, which makes it better:

@[1 +] =evaluator=> @[1 +]

UNPACK can simply say it takes BLOCK! and THE-BLOCK! and you're done. No unquoting and no messing with your head.

I'm thinking this idea of "@[...] represents an already reduced block" is probably something we put in systemically:

>> any [1 + 1 = 2]
== #[true]

>> any @[1 + 1 = 2]
== 1

I think that implies:

>> reduce @[1 + 1 3 + 3]
== @[1 + 1 3 + 3]

Anyway, point remains...I think that UNPACK should have its non-reducing case use THE-BLOCK!.

[op1 op2]: unpack case [
     ... [
        print "This branch uses values as-is"
        @[+ -]
    ]
    ... [
        print "This branch needs evaluation"
        [operators.1, pick [- /] op-num]
   ]
]
1 Like

A post was split to a new topic: Generalized Inertness with @

Thinking of concepts for the dialect, I imagined optionality:

>> [a b c]: unpack [1 2]
** Error: Not enough values

>> [a b /c]: unpack [1 2]
== 1

>> a
== 1

>> b
== 2

>> c
; null

Part of me likes it, but part of me wants to see the refinement style taken more for naming arguments (as in APPLY) and less about optionality. So we'd see /FOO and think "label"...in a way that FOO: can't do because it's too intimately tied up with assignment.

Just something to think about.

Also note that dialect-wise, right now quoting is taken for "variable already exists" when used with a LET.

let [a 'b c]: ...  ; concept is that `b` exists, reuse it, make new a and c

@ is circling, ^ is meta, (expr) is evaluate expression to get the variable to set.

But really I think a lot of attention should be paid on these things, because this is the selling point of the language; building things like this, easily.

2 Likes