I just got bit by a situation where there was something like this:
obj: make object! [
spec: <whatever>
actor: make object! [
thing: function [port] [
spec: any [global.spec, port.spec]
...
]
]
]
I changed this from a FUNCTION to a LAMBDA, forgetting that changing things from a function (today) changes it from doing SET-WORD! gathering.
That means SPEC: went from being a local variable (like LET SPEC:) and referred to the outer SPEC. It overwrote it.
There's a lot going on here, but it reminds me of an idea I've had that perhaps if you are writing code that wants to access object members you identify annotate that as .member
This is something that languages have struggled with. In C++ people are always wondering if they should be clear and write this->member or if you should name members specially like m_member. Reading Rust code today it's often littered with the mandatory self.member until SELF is repeated so often in a method you can't read any of the rest of the code.
I had a lot of trouble deciphering Rebmake due to not knowing what was a function, an object, a member, or a global. It's a big advantage just to have dots for member selection distinct from slashes for refinements. But it would be a bigger advantage to be able to see at a glance that something was a member with the relatively brief dot annotation on .member
As with most things pertaining to binding, I have no real clue how to make it work or cohere. But it's something that has been on my mind--and comes back to my attention every time problems with member variables comes up.
Pure Virtual Binding opened the door to this, so I tried it.
I made the CONSTRUCT primitive a version of MAKE OBJECT! that didn't bind everything inside the block. It only binds the SET-WORDs at the top level, so you can do things like this:
>> x: 10
>> construct [x: x + 10]
== make object! [x: 20]
Then, my "naive" first approach to enable .WORD was simply to make tuple selection stow the object that it picked out of in the function value returned.
>> global: ~
>> obj: construct [x: 10, set $global /foo: does [.x]]
>> obj/x
== 10
>> /test: obj.x/
== ~#[frame! []]~ ; anti
>> test
== 10
>> global
** Error: .x has no object coupling in effect
In the case where you're extracting the action value to store in TEST, the value you get has a pointer to the OBJ object embedded inside it. So it knows how to run it.
But the global variable that was set didn't get that. It just has the uncoupled function. There's a way for you to fix that:
>> /global: couple global/ obj
>> global
== 10
We could make that easier, by letting you pass in a variable to update instead of an "immediate" ACTION! value, e.g. couple $global obj. (Being able to do both might be interesting for more functions of this nature.)
In any case, I don't really have a problem with the fact that doing something like that gives you an "uncoupled" method. That seems fine.
But other things raise questions, some easier to address than others.
What About Enumerations?
obj: make object! [x: 10, /foo: lambda [y] [.x + y]]
for-each [key ~/val] [ ; proposed notation for "maybe action"
if action? val/~ [ ; proposed notation for "don't run, maybe action"
print [val 1000]
]
]
If this is going to work, then the enumeration behind FOR-EACH has to add the coupling.
When To Override?
The current idea is that once an action gets a coupling, it sticks.
>> obj1: make object! [x: 10, /foo: does [.x]]
>> obj2: make object! [x: 20, foo: ~]
>> /obj2.foo: obj1.foo/
>> obj2/foo
== 10
If you don't want this, you'd have to store an UNCOUPLE-d version of the function.
For starters, METHOD and FUNCTION were synonyms. All actions would get "couplings" when picked out of contexts.
This is a bit dodgy in the sense that it seems to put the information in places that it doesn't belong.
So you might argue that if you fetch a function out of a word, then it should get a special state that isn't just "uncoupled", but "anti-coupled", e.g. it won't ever pick up a coupling (until you uncouple it).
In order for that to work, the "anti-coupled" state would have to keep climbing the virtual bind chain vs. stopping at the frame for DOES, to use the .x from FOO.
The problems here don't seem insurmountable...but I'm definitely mulling over whether the picking up of couplings should be limited to a different kind of function, e.g. a METHOD. It seems to me like it's good documentation, but it would help avoid contention:
With a situation like that, it could be the non-method character of DOES which you can use to trust that it won't have a competing meaning for .x
This stuff is all in its early stages, but it really is way more promising than what came before it...
I am pretty sure that CONSTRUCT--not propagating the binding deeply of the object onto everything inside the object--is the better answer for the default of making objects, e.g. {x: x}
The other idea, of making something that spreads its influences deeply, should probably have a name.
CONTEXT wouldn't be a bad name, actually (and has historical precedent as an alias for MAKE OBJECT!), but I'm using that word with another nuance. It's the same functionality as WRAP but just returning the context, so maybe wrap:context could say that you want the context back, not the block?
It's unfortunate in my view that this isn't expressed as:
/bar: method [z] [return /foo z]
The idea of "left hand side is implicit" that you get from .x for a non-method selection would be complemented nicely by /foo for a "left hand side is implicit" member.
Instead, what we get is that we've spent a character on a dot, and yet stayed in the domain of vagueness that a plain WORD! has in terms of whether it's a function call or not.
But staying in that domain of vagueness seems the best choice. I've found it too compelling to say:
/foo: blah blah
And know from that source that FOO is an action antiform, with it not implying it's the member of any object.
So this means if you really want to clarify that you're calling a member function, you'd have to write the relatively ugly:
/bar: method [z] [return /.foo z]
Ick.
Well, for whatever it's worth... I haven't gotten the urge to go around sticking leading slashes on function calls on WORD! just for clarity's sake. I only use it where it has meaning (e.g. PARSE to differentiate combinator invocation vs. plain function invocation with combinator-synthesized arguments). This just feels a little different because it feels like it's a stand in for this.foo, which would error if foo was an action.
But sometimes when you're lining up puzzle pieces you hit these edges, and have to say "it's different when there's nothing to the left".