Hi Chris,
my understanding of a "transparent proxy" is that it automatically forwards all requests, thus requiring special metaprogramming logic to reveal an object as a proxy. On the other hand, "intransparent proxies" would define an explicit list of requests to forward. Not sure however whether this is a useful classification ...
- ProtoObject is still not yet an empty class. For instance, stuff such as #doOnlyOnce: or #scaledIdentityHash should be implemented on Object only. We should continue work on this in the Trunk. You can mitigate this by redefining these messages in your proxy and forwarding them yourself.
It would arguably be a more elegant way of organizing code, but not much else.
One central difference it that only an empty ProtoObject allows you for defining a truly transparent proxy. Otherwise, you are required to override a larger number of messages and forward each of them yourself.
- Primitive methods do not resolve proxies automatically, i.e., #(1 2 3) at: (MyProxy1 for: 2) would have the primitive failed. My mitigation for this problem was to define an exclusion list for certain classes that should never be wrapped into a proxy, but this is not an ideal solution,
My mitigation strategy was a simple "yourself" on the end. It instantly gives away to future developers familiar with writing proxy-aware code. It seems like you're willing to go through to avoid that.
Well, this might depend on the use case of your proxies. I have a proxy class that automatically wraps every answer into another proxy to decorate an entire object graph, so an exclusion list for certain types felt like the right approach to me. Building a framework, I have also no control about to which primitives the client sends these answered instances.
Now you're talkin'! :-) Proxy-aware code!!
I really think this would a small pattern which should be no huge effort to apply to all primitive: sends in the Trunk. :-) How could we best design such a pattern? For instance, looking at Object >> #at:
at: index <primitive: 60> index isInteger ifTrue: [self class isVariable ifTrue: [self errorSubscriptBounds: index] ifFalse: [self errorNotIndexable]]. index isNumber ifTrue: [^self at: index asInteger] ifFalse: [self errorNonIntegerIndex]
We could insert this statement at the beginning of the method:
(index isInteger and: [((thisContext objectClass: index) includesBehavior: Integer) not]) ifTrue: [^ self at: index yourself].
Object>>#perform:withArguments:inSuperclass: could be rewritten like this:
perform: selector withArguments: argArray inSuperclass: lookupClass <primitive: 100> + (selector isSymbol and: [((thisContext objectClass: selector) includesBehavior: Symbol) not]) ifTrue: [^ self perform: selector yourself withArguments: argArray inSuperclass: lookupClass]. (selector isSymbol) ifFalse: [^ self error: 'selector argument must be a Symbol']. (selector numArgs = argArray size) ifFalse: [^ self error: 'incorrect number of arguments']. (self class == lookupClass or: [self class inheritsFrom: lookupClass]) ifFalse: [^ self error: 'lookupClass is not in my inheritance chain']. self primitiveFailed
And so on for almost all the other primitive users as well. For #==, it would be harder because our current semantics of identity say that a proxy has its own identity, and it would probably be a bad decision to change this semantics. Maybe we would need a differentiation between "proxy-aware identity" and "effective identity", and #= (like in Symbol >> #=) would be one of many cases where the latter concept would be required. But complexity is increasing right now ...
See also the discussion about the right place for the mirror protocol. [1] Any other ideas how we could minify/beautify the above pattern? :-)
Best, Christoph
[1] http://lists.squeakfoundation.org/pipermail/squeak-dev/2022-January/218221.h...
--- Sent from Squeak Inbox Talk
On 2022-01-15T18:51:00-06:00, asqueaker@gmail.com wrote:
Hi Craig,
Indeed, none of our code should be proxy-aware.
I certainly would agree with the statement, "it would be nice if none of our code needed to be proxy-aware," but I don't yet understand how it's possible, regardless where the implementation details are handled (VM or image). As a VM expert, I can appreciate you preferring to use VM-based proxies. The only downside is that those who only understand Smalltalk and not the secret innards of the VM cannot realistically participate in Squeak anymore if they wish or need to use the classic Proxy pattern. We broke the code and also legacy systems for that.
Which is why I keep bringing up just this one example on Symbol and hoping someone can explain it to help me get unstuck. Would you follow its code with me, line by line, below? Let's pretend the receiver is the symbol #size, and the argument, aSymbol, comes in as an instance of MyProxy, which refers to an Integer oid that ultimately will reify to the receiver object (#size)).
It's easy to see why the code no longer works for the traditional Proxy pattern:
= aSymbol "<------ aSymbol comes in as a MyProxy instance" self == aSymbol ifTrue: [^ true]. "<---- false, continue" aSymbol isSymbol ifTrue: [^ false]. "<----- true, return false. BUG!" "Use String comparison otherwise" ^ super = aSymbol
So what about when aVMProxy instance is passed in? I'm so used to normal Smalltalk-think, I'm not even able to think about WHEN or HOW the VM would perform it's magic handling of the incoming VMProxy instance to make this work. I guess this is a trade-off -- we don't have to think about Proxy's, but we have to know about and think about VM magic. I'm willing to try, but I don't even know where to begin. Would you help?
Regards, Chris
my understanding of a "transparent proxy" is that it automatically forwards all requests, thus requiring special metaprogramming logic to reveal an object as a proxy. On the other hand, "intransparent proxies" would define an explicit list of requests to forward. Not sure however whether this is a useful classification ...
Then there is a balance between those two, traditional "Proxy", which defines an explicit list of requests NOT to forward, any message not in the list is automatically forwarded.
That explicit list of requests is the API to the proxy, the "metaprogramming logic" you alluded to, above. As a Smalltalker, I like to have that API defined on ProtoObject with "__" or "basic" prefixes; i.e., #__class or #basicClass. There could be one for each mirror primitive, and nothing else. That's my preferred aesthetic, but the aesthetic of the Proxy API is less important to me than the handling semantics needed throughout the code.
- ProtoObject is still not yet an empty class.
For instance, stuff such as #doOnlyOnce: or #scaledIdentityHash should be implemented on Object only.
That means Proxies could not be members of any HashedCollections without forcing reification.
However, I agree it's a lot easier and safer for a user to add that one than remove it.
We should continue work on this in the Trunk. You can mitigate this by redefining these messages in your proxy and forwarding them yourself.
Agreed.
I really think this would a small pattern which should be no huge effort to apply to all primitive: sends in the Trunk. :-) How could we best design such a pattern? For instance, looking at Object >> #at:
...
We could insert this statement at the beginning of the method:
(index isInteger and: [((thisContext objectClass: index) includesBehavior: Integer) not]) ifTrue: [^ self at: index yourself].
...
Object>>#perform:withArguments:inSuperclass: could be rewritten like this:
perform: selector withArguments: argArray inSuperclass: lookupClass <primitive: 100> + (selector isSymbol and: [((thisContext objectClass: selector) includesBehavior: Symbol) not]) ifTrue: [^ self perform: selector yourself withArguments: argArray inSuperclass: lookupClass]. (selector isSymbol) ...
That looks good. Regardless of the specific implementation details, +1000 to the *concept* of allowing Squeak to support the classic Proxy pattern! And it should be pushed as low into the code as it can to maximize usage transparency. (I realized your definition of "transparent" was a function of the implementation, mine is a function of their usage).
I'm not against a VM-based proxy system at all, the more options the better. It's just that we've allowed our anticipation of it to allow Squeak's support for the classic Smalltalk Proxy pattern to get broken (because of Symbol>>#=), so we currently have no options at all.
[1] http://lists.squeakfoundation.org/pipermail/squeak-dev/2022-January/218221.h...
Thanks! Chris
Hi Chris, list,
I thought I'd add my voice in, as I've implemented pluggable proxies.
On Mon, Jan 17, 2022, 14:43 Chris Muller asqueaker@gmail.com wrote:
my understanding of a "transparent proxy" is that it automatically
forwards all requests, thus requiring special metaprogramming logic to reveal an object as a proxy. On the other hand, "intransparent proxies" would define an explicit list of requests to forward. Not sure however whether this is a useful classification ...
Then there is a balance between those two, traditional "Proxy", which defines an explicit list of requests NOT to forward, any message not in the list is automatically forwarded.
In my proxy code, I have this sort of "intercepting" proxy. There are also "before" and "after" message echo proxies, used for enforcing constraints and running triggers. I used it to build an object system.
That explicit list of requests is the API to the proxy, the
"metaprogramming logic" you alluded to, above. As a Smalltalker, I like to have that API defined on ProtoObject with "__" or "basic" prefixes; i.e., #__class or #basicClass. There could be one for each mirror primitive, and nothing else. That's my preferred aesthetic, but the aesthetic of the Proxy API is less important to me than the handling semantics needed throughout the code.
I agree! It needs to be very easy to use proxies. The more invisible they are, the better. If it's too difficult or results in rather un-Smalltalk code, people won't want to use them.
- ProtoObject is still not yet an empty class.
For instance, stuff such as #doOnlyOnce: or #scaledIdentityHash
should be implemented on Object only.
That means Proxies could not be members of any HashedCollections without forcing reification.
I disagree. They can still participate in identity hashing, as they have an identity. More on that later. Especially, the biggest use I have found for proxies is query result sets and efficient enumeration.
And it should be pushed as low into the code as it can to
maximize usage transparency. (I realized your definition of "transparent" was a function of the implementation, mine is a function of their usage).
I don't like adding more things a garbage collector needs to know about, but between MNU overhead and #== being "no lookup", a functionally transparent proxy needs to happen at that level.
I'm not against a VM-based proxy system at all, the more options the better.
So long as it can be configured by object-level means, like subclasses, I prefer VM support over "pretty transparent but has gotchas". Needing to install the latest VM is not a big deal to me compared to reconfiguring an image.
Also, could it be offered as external?
It's just that we've allowed our anticipation of it to allow
Squeak's support for the classic Smalltalk Proxy pattern to get broken (because of Symbol>>#=), so we currently have no options at all.
The way I looked at it was, a proxy is invisible to the object system. It doesn't exist. You can't test for it with #==, because it passes #== through itself. But, it does have an identity, since it's a 1st Class object, so you can ask *aProxy identityHash = aHash* and get something sensible out of it.
(It was a little more complicated than that. When you called #new the outermost proxy would be a anObject, which does understand #==. None of the internal components understood #== unless they were also anObject, which meant you had a chain. But that's for multiple inheritance.)
From, Lauren
Hi all,
That means Proxies could not be members of any HashedCollections without forcing reification.
Which just would be consequent if proxies do not have an own identity. This sounds to me like an optimization issue, though an important one, but not like a conceptual problem. If you want to create a proxy with its own identity, you could subclass an IdentityProxy from ProtoObject. :-)
Anyway, in SimulationStudio's Sandbox, I am using a pluggable dictionary that uses mirror primitives for the hash and equality blocks. But that might only make sense in the context of simulation.
Yes, ok. But better would be to introduce a set of selectors understood by ProtoObject and the relevant classes for checking arguments to primitives etc, i.e. for checking concrete types. So ProtoObject and SmallInteger could implement isBasicSmallInteger and the code would read
*at:* index <*primitive:* 60> index isBasicSmallInteger ifTrue: [self basicClass isVariable ifTrue: [self errorSubscriptBounds: index] ifFalse: [self errorNotIndexable]]. index isNumber ifTrue: [^self at: index asInteger] ifFalse: [self errorNonIntegerIndex]
What is your conception of "selectors understood by ProtoObject"? I don't think ProtoObject should implement these messages itself (it probably wasn't even able to do so). One problem that I see with both #yourself and #asInteger is that it is not obvious for the programmer of a proxy that exactly these messages must not return proxies. Maybe a single, special selector like #asNonProxy would be more helpful? Or at least #asBasicSmallInteger? As mentioned earlier, I have a proxy that automatically answers new proxies via #doesNotUnderstand:. It should be as easy as possible to distinguish between messages that may answer a proxy and messages that must answer a VM-compatible object.
Other than that, #isBasicSmallInteger sounds reasonable. :-)
By the way, there are some other selectors with are not proxy-safe at the moment. For instance, myProxiesBoolean ifTrue: [...] would currently raise NonBooleanReceiver. But conceptually, this is no new issue. :-)
I like the overall ideas in this thread. I'm looking forward to addressing this after the release. :-)
Best, Christoph
--- Sent from Squeak Inbox Talk
On 2022-01-18T00:48:50-07:00, drurowin@gmail.com wrote:
Hi Chris, list,
I thought I'd add my voice in, as I've implemented pluggable proxies.
On Mon, Jan 17, 2022, 14:43 Chris Muller <asqueaker at gmail.com> wrote:
my understanding of a "transparent proxy" is that it automatically
forwards all requests, thus requiring special metaprogramming logic to reveal an object as a proxy. On the other hand, "intransparent proxies" would define an explicit list of requests to forward. Not sure however whether this is a useful classification ...
Then there is a balance between those two, traditional "Proxy", which defines an explicit list of requests NOT to forward, any message not in the list is automatically forwarded.
In my proxy code, I have this sort of "intercepting" proxy. There are also "before" and "after" message echo proxies, used for enforcing constraints and running triggers. I used it to build an object system.
That explicit list of requests is the API to the proxy, the
"metaprogramming logic" you alluded to, above. As a Smalltalker, I like to have that API defined on ProtoObject with "__" or "basic" prefixes; i.e., #__class or #basicClass. There could be one for each mirror primitive, and nothing else. That's my preferred aesthetic, but the aesthetic of the Proxy API is less important to me than the handling semantics needed throughout the code.
I agree! It needs to be very easy to use proxies. The more invisible they are, the better. If it's too difficult or results in rather un-Smalltalk code, people won't want to use them.
- ProtoObject is still not yet an empty class.
For instance, stuff such as #doOnlyOnce: or #scaledIdentityHash
should be implemented on Object only.
That means Proxies could not be members of any HashedCollections without forcing reification.
I disagree. They can still participate in identity hashing, as they have an identity. More on that later. Especially, the biggest use I have found for proxies is query result sets and efficient enumeration.
And it should be pushed as low into the code as it can to
maximize usage transparency. (I realized your definition of "transparent" was a function of the implementation, mine is a function of their usage).
I don't like adding more things a garbage collector needs to know about, but between MNU overhead and #== being "no lookup", a functionally transparent proxy needs to happen at that level.
I'm not against a VM-based proxy system at all, the more options the better.
So long as it can be configured by object-level means, like subclasses, I prefer VM support over "pretty transparent but has gotchas". Needing to install the latest VM is not a big deal to me compared to reconfiguring an image.
Also, could it be offered as external?
It's just that we've allowed our anticipation of it to allow
Squeak's support for the classic Smalltalk Proxy pattern to get broken (because of Symbol>>#=), so we currently have no options at all.
The way I looked at it was, a proxy is invisible to the object system. It doesn't exist. You can't test for it with #==, because it passes #== through itself. But, it does have an identity, since it's a 1st Class object, so you can ask *aProxy identityHash = aHash* and get something sensible out of it.
(It was a little more complicated than that. When you called #new the outermost proxy would be a anObject, which does understand #==. None of the internal components understood #== unless they were also anObject, which meant you had a chain. But that's for multiple inheritance.)
From, Lauren
Hi Christoph, Hi All,
On Sun, Jan 16, 2022 at 9:21 AM christoph.thiede@student.hpi.uni-potsdam.de wrote:
Hi Chris,
my understanding of a "transparent proxy" is that it automatically forwards all requests, thus requiring special metaprogramming logic to reveal an object as a proxy. On the other hand, "intransparent proxies" would define an explicit list of requests to forward. Not sure however whether this is a useful classification ...
- ProtoObject is still not yet an empty class. For instance, stuff
such as #doOnlyOnce: or #scaledIdentityHash should be implemented on Object only. We should continue work on this in the Trunk. You can mitigate this by redefining these messages in your proxy and forwarding them yourself.
It would arguably be a more elegant way of organizing code, but not much
else.
One central difference it that only an empty ProtoObject allows you for defining a truly transparent proxy. Otherwise, you are required to override a larger number of messages and forward each of them yourself.
+1. Squeak's ProtoObject has far too many methods. It should have doesNotUnderstand: and nothing else except selectors that are understood to be proxy-specific and not for use in "normal" code.
- Primitive methods do not resolve proxies automatically, i.e., #(1 2
- at: (MyProxy1 for: 2) would have the primitive failed. My mitigation for
this problem was to define an exclusion list for certain classes that should never be wrapped into a proxy, but this is not an ideal solution,
My mitigation strategy was a simple "yourself" on the end. It instantly
gives away to future developers familiar with writing proxy-aware code. It seems like you're willing to go through to avoid that.
Well, this might depend on the use case of your proxies. I have a proxy class that automatically wraps every answer into another proxy to decorate an entire object graph, so an exclusion list for certain types felt like the right approach to me. Building a framework, I have also no control about to which primitives the client sends these answered instances.
Now you're talkin'! :-) Proxy-aware code!!
I really think this would a small pattern which should be no huge effort to apply to all primitive: sends in the Trunk. :-) How could we best design such a pattern? For instance, looking at Object >> #at:
*at:* index <*primitive:* 60> index isInteger ifTrue: [self class isVariable ifTrue: [self errorSubscriptBounds: index] ifFalse: [self errorNotIndexable]]. index isNumber ifTrue: [^self at: index asInteger] ifFalse: [self errorNonIntegerIndex]
We could insert this statement at the beginning of the method:
(index isInteger and: [((thisContext objectClass: index)
includesBehavior: Integer) not]) ifTrue: [^ self at: index yourself].
Yes, ok. But better would be to introduce a set of selectors understood by ProtoObject and the relevant classes for checking arguments to primitives etc, i.e. for checking concrete types. So ProtoObject and SmallInteger could implement isBasicSmallInteger and the code would read
*at:* index <*primitive:* 60> index isBasicSmallInteger ifTrue: [self basicClass isVariable ifTrue: [self errorSubscriptBounds: index] ifFalse: [self errorNotIndexable]]. index isNumber ifTrue: [^self at: index asInteger] ifFalse: [self errorNonIntegerIndex]
Object>>#perform:withArguments:inSuperclass: could be rewritten like this:
*perform:* selector *withArguments:* argArray *inSuperclass:*
lookupClass <*primitive:* 100> + (selector isSymbol and: [((thisContext objectClass: selector) includesBehavior: Symbol) not]) ifTrue: [^ self perform: selector yourself withArguments: argArray inSuperclass: lookupClass]. (selector isSymbol) ifFalse: [^ self error: 'selector argument must be a Symbol']. (selector numArgs = argArray size) ifFalse: [^ self error: 'incorrect number of arguments']. (self class == lookupClass or: [self class inheritsFrom: lookupClass]) ifFalse: [^ self error: 'lookupClass is not in my inheritance chain']. self primitiveFailed
And so on for almost all the other primitive users as well. For #==, it would be harder because our current semantics of identity say that a proxy has its own identity, and it would probably be a bad decision to change this semantics. Maybe we would need a differentiation between "proxy-aware identity" and "effective identity", and #= (like in Symbol >> #=) would be one of many cases where the latter concept would be required. But complexity is increasing right now ...
See also the discussion about the right place for the mirror protocol. [1] Any other ideas how we could minify/beautify the above pattern? :-)
Best, Christoph
[1] http://lists.squeakfoundation.org/pipermail/squeak-dev/2022-January/218221.h...
*Sent from **Squeak Inbox Talk https://github.com/hpi-swa-lab/squeak-inbox-talk*
On 2022-01-15T18:51:00-06:00, asqueaker@gmail.com wrote:
Hi Craig,
Indeed, none of our code should be proxy-aware.
I certainly would agree with the statement, "it would be nice if none of our code needed to be proxy-aware," but I don't yet understand how it's possible, regardless where the implementation details are handled (VM or image). As a VM expert, I can appreciate you preferring to use VM-based proxies. The only downside is that those who only understand Smalltalk and not the secret innards of the VM cannot realistically participate in Squeak anymore if they wish or need to use the classic Proxy pattern. We broke the code and also legacy systems for that.
Which is why I keep bringing up just this one example on Symbol and hoping someone can explain it to help me get unstuck. Would you follow its code with me, line by line, below? Let's pretend the receiver is the symbol #size, and the argument, aSymbol, comes in as an instance of MyProxy, which refers to an Integer oid that ultimately will reify to the receiver object (#size)).
It's easy to see why the code no longer works for the traditional Proxy
pattern:
= aSymbol "<------ aSymbol comes in as a MyProxy instance" self == aSymbol ifTrue: [^ true]. "<---- false, continue" aSymbol isSymbol ifTrue: [^ false]. "<----- true, return false. BUG!" "Use String comparison otherwise" ^ super = aSymbol
So what about when aVMProxy instance is passed in? I'm so used to normal Smalltalk-think, I'm not even able to think about WHEN or HOW the VM would perform it's magic handling of the incoming VMProxy instance to make this work. I guess this is a trade-off -- we don't have to think about Proxy's, but we have to know about and think about VM magic. I'm willing to try, but I don't even know where to begin. Would you help?
Regards, Chris
squeak-dev@lists.squeakfoundation.org