ARRAY/INITIAL and ACTION! Isotopes

So the ARRAY mezzanine from R3-Alpha allowed you to make an array with an initial value that could be a function...in which case it evaluated that function each time:

r3-alpha>> array 3
== [none none none]

r3-alpha>> array/initial 3 10
== [10 10 10]

r3-alpha>> n: 0 array/initial 3 does [n: n + 1]
== [1 2 3]

This raises the question of how you would make an array whose initial value was itself a function.

If you're a loophole-minded person, you might notice you could trick this particular case by making a function that returns a function.

r3-alpha>> array/initial 3 does [:append]
== [#[action! [...]] #[action! [...]] #[action! [...]]]

But that's not really a general answer for how to deal with this kind of polymorphism--it just works if you're using the result purely to do a substitution.

Trying This With Antiform Actions

I've loosened my stance on isotopes in function frames as parameters. It's not a good idea to take them as default, but forcing you to make every parameter ^META just to get isotopes is limiting.

So I gave this a shot...

array: func [
    {Makes and initializes a block of a given size}

    return: "Generated block or null if blank input"
        [block!]
    size "Size or block of sizes for each dimension"
        [<maybe> integer! block!]
    /initial "Initial value (will be called each time if action isotope)"
        [any-element? action?]
]

So that [any-element? action?] leads to a willingness to accept frame antiforms, as well as anything you can put in a block. Having such an annotation leads you into the "danger zone" where if you don't prefix accesses to INITIAL with a colon, you run the risk of running a function. But if you don't have the annotation, you don't have to be paranoid and decorate your references.

Here's how the INITIAL references wind up in the implementation:

case [
    block? rest [
        repeat size [append block (array/initial rest :initial)]
    ]
    action? :initial [
        repeat size [append block run :initial]  ; Called every time
    ]
    any-series? initial [
        repeat size [append block (copy/deep initial)]
    ]
] else [
    append/dup block initial size
]

So the BLOCK! case which recurses uses the :INITIAL for pass-thru.

I used RUN :INITIAL when I could have just used INITIAL, simply because it feels more clear. If this wasn't already contained by being at the end of a block, it would be better to limit the parameterization by saying something like apply :initial [] or the now-equivalent initial :: []

And It Works

What we're kind of accepting as a default is that function generation produces isotopes. So getting a plain action requires some kind of extra step, like REIFY

>> func [x] []
== ~#[frame! [x]]~  ; anti

>> action: reify func [x] []
== ~#[frame! {action} [x]]~

>> action  ; not word!-active
== ~#[frame! {action} [x]]~

>> ap: reify :append
== ~#[frame! {ap} [series value /part /dup /line]]~

>> ap
== ~#[frame! {ap} [series value /part /dup /line]]~

What this means is that you get a similar behavior to before, where if you pass a "live" function you've fetched from a word or just generated, then it runs. You have to do something extra to it to get it to be inert.

>> array 3
== [_ _ _]

>> array/initial 3 10
== [10 10 10]

>> n: 0 array/initial 3 does [n: n + 1]
== [1 2 3]

>> array/initial 3 reify does [n: n + 1]
== [~#[frame! []]~ ~#[frame! []]~ ~#[frame! []]~]

>> array/initial 3 reify/unquasi does [n: n + 1]
== [#[frame! []] #[frame! []] #[frame! []]]

That Looks Like An Isotope Success Story...

It doesn't require any convoluted thinking to get the literal vs. non-literal distinction. And you can apply this technique to cases that don't have a weird workaround.

I wish I could say that all of antiform action impacts were as well sorted out as this looks. But it's a start, and it shows the potential.

2 Likes