A Dream For Debugging: Generalized Accessors

You may be familiar in some languages with the ideas of "Getters" and "Setters" (more generally called "Accessors"). These features make it so that when you do what looks like normal field access, you're actually running code to do the fetches and assignments.

A Rebol version of this idea might look a bit like:

obj: make object! [
    writable-private: 1020
    readable-private: [some block]

    writable: accessor func [^:value [integer!]] [  ; new :refinement
        if not value [  ; it's a get (meta protocol discerns from set to null)
            print "getting writable!"
            return .writable-private
        ]
        else [  ; it's a set
            print "setting writable!"
            return .writable-private: unmeta value
        ]
    ]

    readable: accessor does [  ; not taking ^:value means read only
        print "getting readable"
        .readable-private.  ; (terminal dot means "any value, including actions")
    ]
]

>> obj.writable
getting writable!
== 1020

>> obj.writable: 304
setting writable!
== 304

>> obj.readable
getting readable!
== [some block]

>> obj.readable: [some other block]
** Error: Cannot assign obj.readable (read only)

The sky is the limit for how useful this can be. You already see that it can be used to implement typechecking. But really anything else...

Debugging is clearly a place this can help a lot

You can intercept reads, and you can intercept writes even of particular values...

While I am still designing to facilitate stepwise debugging correctly, this is actually better than stepwise debugging for solving many kinds of problems. It gives you a completely generic hook.

Leagues Better Than Red's ON-CHANGE*

The writable checks are a more granular interface to what Red provides as ON-CHANGE*. Red's idea is that's something you put in your object which is a function that gets the name of a member that's changing. You'd have to put in a SWITCH statement on the name, and notice when it's the field you're interested in. It's clumsy, doesn't help you when variables are being read vs. written, and blesses a name of a method in your object.

But also...

Generalized Accessors means NO OBJECT IS REQUIRED!

The way I'm looking at implementing this, you can do it to any variable.

>> foo: accessor func [<static> value (100) [print "Plain old foo!", value]]

>> foo
Plain old foo!
== 100

:heart_eyes:

"You want it.
You need it.
You've got to got to have it!"

(Though for reasons of implementation sanity, this will not be allowed on variables in the LIB context, where the system relies too heavily on the native definitions being literal.)

Not Finished Yet...But Proof-Of-Concept Is Working

Carl wanted them, but acknowledged the difficulty:

I should mention, however, it's not that easy to add set-functions. If it were really easy, we'd have them. The problem is that set-functions add another branch to the tree of REBOL language semantics. For example, how are set functions defined and how reflective are they?

But if anything, he's underestimating the difficulty of adding them to the codebase as he had it. If you know anything about the implementation, you should ask "how can you put an action in a cell and not screw up by having some part of the code think the action is the variable itself...not an action to call to get the variable?"

The answer is that the flag which is put on the cell systemically inhibits it from being read or written by "normal" functions. Assertions will be triggered if the wrong level of abstraction tries to handle it. R3-Alpha would have had to be completely overhauled for such checks. But that part doesn't have to be added to Ren-C because it was built in long ago (to make things like CONST not have holes in it).

Anyway, it will take a while. But the method looks like it will have means to perform well enough to be the answer to typechecking on reads and writes where needed...which is something I think we do need for the language to be modern enough to be applied seriously. (All languages seem to evolve into typed versions when they hit problems at scale, e.g. JavaScript => TypeScript)

2 Likes

So it occurs to me that this is a case where you don't accept NULL assignments--only integer ones--and hence there's no conflation of NULL for being a GET vs. a null SET...

...if we made a general policy that refinements would not accept NULL unless they were ^META, then we could allow cases like this to be non-meta.

writable: accessor func [:value [integer!]] [...]

The error on trying to pass a null to the refinement could come from the function machinery itself. Then, the accessor would simply notice the parameter convention on :value and pass it ^META on an as-needed basis.

What I mean to say is--that even if your typeset included NULL (e.g. ANY-VALUE?)--that it would be an error... just as it's an error to be passed an action isotope if you don't have a slash.

I don't know how many accessors would be type constrained to exclude null. But quite possibly a sizable number of them would. So this could make peoples' lives easier...