Magic Statics: The Statics You've Always Wanted!

HIstorical Rebol had a quirky trick for implementing static variables, using self-modifying code.

rebol2>> accumulate: func [value /local store] [
             store: [0]
             store/1: store/1 + value
             return store/1
         ]

rebol2>> accumulate 10
== 10

rebol2>> accumulate 20
== 30

The bizarre thing that's happening here is that each time the function runs, the store variable receives the same BLOCK! identity...a single instance living in the function body.

That gets modified, so if you look at the source, you see it has been updated:

rebol2>> source accumulate
accumulate: func [value /local store][
    store: [30]
    store/1: store/1 + value
    return store/1
]

Ren-C has safety mechanisms with CONST to stop you from writing self-modifying code on accident:

https://rebol.metaeducation.com/t/value-vs-series-modification-bit-const-and-mutable/976

But you can still do this, you just have to say MUTABLE on the block.


Ren-C Statics Dial This Up To 11

>> accumulate: func [value] {
       store: static [print "Initializing!" 0]
       store: store + value
       return store
   }

>> accumulate 10
Initializing!
== 10

>> accumulate 20
== 30

Not only can you run arbitrary initialization, but you aren't limited to a static variable that's a BLOCK!... your static can be any kind of value you want!

:exploding_head:


So How Does It Work?

It's based on the same principle of mutating the code. But what it does is it mutates the block to hold a variable for static storage. Then, the STATIC function sets up an aliasing relationship between the variable you're assigning the static to and the storage. On subsequent calls it sets up that same relationship.

static: lambda [
    "Create static variable with lasting identity across multiple invocations"
    []: [dual?]
    @init [block! fence!]
][
    if init <> '[static-storage] {  ; first run if not equal
        static-storage: eval init   ; v-- mutate to [static-storage]
        insert clear mutable init $static-storage
    }
    alias init.1
]

It's a bit more expensive than other approaches. But once it's nativized it won't be terrible. It creates an extra variable but only one per static, and since it's static you won't be doing it on every call.

It's slick enough that I'll be using it often in non-performance-critical code!

1 Like

It occurs to me that we could have a version of this that doesn't make variables (so no SET-ability), but just caches calculations.

cache: lambda [
    "Calculate result once, return cached result on successive evals"
    []: [any-value?]
    @init [block! fence!]
][
    if init <> '[^cache-storage] {  ; first run if not equal
        ^cache-storage: eval init  ; v-- mutate to [^cache-storage]
        insert clear mutable init meta $cache-storage  
    }
    get init.1
]

That one's doable in Rebol2/Red, even.

>> foo: lambda [x] [x + cache [print "Caching!" 1000 + 20]]

>> foo 1
Caching!
== 1021

>> foo 2
== 1022

Pretty slick!

1 Like

This is using something I mentioned in the past that is a little scary to me... which is the idea of unlifted things in PACK! meaning "assign in the dual band".

>> alias $foo
== \~[foo]~\  ; antiform (pack!) "dual"

It's a creepy idea. Creepy enough that I would say it doesn't count as an ANY-VALUE? and by default functions can't return these on accident. Hence you have to say return: [dual?] or somesuch.

It does raise the question of what the assignment synthesizes.

If you say x: y: static [10 + 20] what is X? Is it another name for the static, or is it an independent integer holding 30?

Or is it too dangerous to pick any specific choice, so the assignment of a dual state produces TRASH!, and if you're sure you know what you're doing you go through a lift?

temp: lift static [10 + 20]
x: unlift temp
y: unlift temp

:thinking:

For the moment, duals propagate like anything else. So when a dual alias is assigned to multiple places, you get multiple aliases:

>> x: y: static [10 + 20]
== \~[^static-storage]~\  ; antiform (pack!) "dual: alias"

>> x
== 30

>> y
== 30

>> x: 1020
== 1020

>> y
== 1020

And if you decay the alias, you will get the number:

>> x: decay y: static [10 + 20]
== 30

>> x
== 30

>> y
== 30

>> x: 1020
== 1020

>> y
== 30

Pretty wild. But, if this turns out to be crazy, maybe producing trash after an assignment of a dual is better.

Note that one fairly clean alternative approaches is to just wrap the code in a FENCE!

accumulate: eval {
    store: 0

    func [value] [
        store: store + value
        return store
    ]
}

The thing I don't like about this is that when you have a lot of statics, it creates a big gap between the name of the function you're assigning and the spec of the function being stored there.

You can work around this by using a GROUP! with a FENCE! in it as the body, and synthesize a BLOCK! or FENCE! for the code at the end:

accumulate: func [value] ({
    store: 0
    [
        store: store + value
        return store
    ]
})

But note that if you wanted to use a FENCE! as the body of the function, you can't evaluate it before the FUNC sees it:

accumulate: func [value] ({
    store: 0
    ${
        store: store + value
        return store
    }
})

I'm wary of making something like an arity-3 FUNC-STATIC:

accumulate: func-static [value] [
    store: 0
] {
    store: store + value
    return store
}

The reason is that then you have to make it for everything else (PROC-STATIC, LAMBDA-STATIC, etc. etc.)

Maybe we will go back to a way to put statics in the spec, I'll think on it.

1 Like