Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object method identification: convention or requirement? #54

Open
gibson042 opened this issue May 23, 2023 · 7 comments
Open

Object method identification: convention or requirement? #54

gibson042 opened this issue May 23, 2023 · 7 comments

Comments

@gibson042
Copy link
Collaborator

Originally posted by @gibson042 in #42 (comment)

Agoric arguably promotes this convention into a requirement in the sense that every inbound delivery message is interpreted as a [method, arguments] array in which the method is either undefined (a special case corresponding with direct function invocation) or is coerced to a JavaScript property key (i.e., either preserved as a symbol or coerced to a string) and interpreted as identifying a method of the target object, and e.g. the coercion of any pass-by-copy record or other complex data (if successful) would destructively conflate it with the string "[object Object]".

@zenhack
Copy link
Collaborator

zenhack commented May 24, 2023

It might be nice if property access and function invocation were orthogonal, though I know this isn't how js works wrt. this.

@tsyesika
Copy link
Contributor

tsyesika commented Jun 1, 2023

Goblins doesn't prescribe a perspective on how methods work, or even if they are used. It's not an exception to the rule that actors are just lambdas and have no methods.

It might be nice if property access and function invocation were orthogonal, though I know this isn't how js works wrt. this.

I'm not sure what you mean, could you elaborate?

@cwebber
Copy link
Contributor

cwebber commented Jun 1, 2023

Early in the pre-pre-standardization state of discussing OCapN, I had a talk with @erights about this. Since Goblins takes a "lambda, the ultimate" view, and Agoric's JS decends from the E view that "method-objects are the ultimate", obviously there's a mismatch. But what we had discussed as a solution, per my memory, is that since Agoric doesn't really use symbols anyway, this can be detected as an indicator where if the first argument is a symbol, that's conventionally the same as calling a method. So foo(Symbol("bar"), "baz") would be translated into foo.bar("baz") at the perimeter more or less. This should allow both to work fine, and there's really only one exception, which is if an object has both "procedure-like behavior" and methods both, but those kinds of APIs tend to be more rare (I know Python supports them, I think JS does too but I forget).

@gibson042
Copy link
Collaborator Author

I don't think that works, because both strings and symbols can be used as JS property keys (e.g., foo.bar("baz") is equivalent to foo['bar']("baz") but distinct from foo[Symbol('bar')]("baz"), although all three are valid attempts to invoke a method) and at least one symbol is in active use by Agoric to identify a method—the built-in Symbol.asyncIterator (which appears on objects that implement async iteration, most likely corresponding with your memory of "procedure-like behavior").

@zenhack
Copy link
Collaborator

zenhack commented Jun 1, 2023

I'm not sure what you mean, could you elaborate?

Re: keeping property access orthogonal, my thinking was that it might be nice if whatever js maps to the operations foo.bar and baz(quux) to are orthogonal at the protocol level. I'd been thinking about a scheme where, if the receiver was an object, and you sent a call that was a single symbol or string argument, that argument was used to access a field, so foo.bar if you sent the string "bar" to object foo. And if the receiver was a function, you'd just call it.

regarding this, I was referring to the fact that these are not actually equivalent in js:

foo.bar()        // In the call to bar, `this` is bound to foo.
f = foo.bar; f() // `this` is `undefined`.

There's also a separate problem with this, which is that functions can themselves have properties:

f = foo.bar
f.x = 2
// What should sending "x" to f do?

@kriskowal
Copy link
Collaborator

I’m weighing in on Team Method Invocation Normalization is a Requirement.

An idiomatic Guile Goblin Object is very similar to an Endo JavaScript Far Object. These are approximately equivalent.

(define actor1
  (methods
    (('method1 arg1 arg2) "method1 called")
    (('method2 arg1 arg2) "method2 called")))

(actor1 'method1 arg1 arg2)
(actor2 'method2 arg1 arg2)
const actor1 = Far('Actor', {
  method1: (arg1, arg2) => "method1 called",
  method2: (arg1, arg2) => "method2 called",
});

E(actor1).method1(arg1, arg2);
E(actor2).method2(arg1, arg2);

However, if we do not normalize invocation in O’Cap’n, such that the wire representation of a method name is the same regardless of peer languages, the idioms for calling across languages will be different, as well as the idioms for defining actors.

(actor1 "method1" arg1 arg2)
(actor1 "method2" arg1 arg2)
E(actor1)[Symbol.for('method1')](arg1, arg2)
E(actor2)[Symbol.for('method2')](arg1, arg2)

To emulate a JavaScript actor in Scheme, you’ll need to drop below the methods macro and write something like:

(lambda (method arg1 arg2)
  (cond method
    ((equal? method "method1") "called method1")
    ((equal? method "method2") "called method2")))

And, likewise, to emulate a Scheme actor in JavaScript:

const actor1 = Far('Actor', {
  [Symbol.for('method1'): (arg1, arg2) => "method1 called",
  [Symbol.for('method2'): (arg1, arg2) => "method2 called",
});

The following JavaScript and Scheme actors are approximately equivalent bare functions and respective calling conventions:

(define actor3 (method arg1 arg2)
  (if (equal? method #f) "called as a function"))

(actor3 #f arg1 arg2)
const actor3 = Far('Actor', (arg1, arg2) => "called as a function");

E(actor3)(arg1, arg2);

However, the following Scheme actor is not expressible in JavaScript because it implements a heterogenous mix of function and object behaviors, including JavaScript and Scheme idioms for method names.

(define actor4 (method arg1 arg2)
  (cond method
    ((equal? method #f) "called-as-function")
    ((equal? method 'symbol) "called-with-symbol")
    ((equal? method "string") "called-with-string")))

(actor4 #f)
(actor4 'symbol)
(actor4 "string")

The following JavaScript actor cannot be called from Scheme because it implements both a well-known-symbol and a registered-symbol. These are disjoin namespaces. Scheme could presumably call this JavaScript method using a tagged type. I do not know how that would look, but it would not be pretty.

const actor5 = Far('Actor', {
  [Symbol.asyncIterator): (arg1, arg2) => "asyncIterator well-known symbol called",
  [Symbol.for('asyncIterator'): (arg1, arg2) => "asyncIterator registered symbol called",
});

These complications come to bear especially when O’Cap’n begins defining protocols like the bootstrap object for three-party-handoff, which effectively requires every language to speak to every other language using the Scheme actor idiom.

E(bootstrap)[Symbol.for('deposit-gift')](gift)
(bootstrap 'deposit-gift gift)

I would like to arrive at an agreement that:

  1. O’Cap’n implementations must be mutually able to emulate one another.
  2. O’Cap’n protocols should be mutually implementable, ideally even expressed idiomatically.
  3. Behaviors that one O’Cap’n implementation cannot emulate should not be expressible over the O’Cap’n wire protocol.
  4. Conventional protocols across languages should not encode idioms of the language of the sender or receiver, like symbol vs string name methods. This cannot be deferred since O’Cap’n defines a bootstrap protocol for 3PH.

I wish to then convince you:

  1. These requirements alone preclude representing symbols on the wire core data types: symbol? #46.
  2. These requirements suggest that we use the inter-language common ground of dromedaryCase method names on the wire.
  3. It undue burden if, in every language, an actor can provide more than one of the following behaviors:
  4. function application
  5. method invocation with an interned symbol-named method
  6. method invocation with a stringly named method
  7. It would be an undue burden on Goblins to have to differentiate well-known symbols from registered-symbols. I’m sure we already agree gensyms and unregistered symbols have no place on the wire.
  8. It would be an undue burden on Python and C if they had to support objects with both stringly and symbolicaly named methods.

I move:

  1. to represent invocation as a special form in O’Cap’n such that the sender and receiver see the invocation according to their own language’s idioms for symbols vs strings and also dromedaryCase.
  2. to record these on the wire as a field of type “method” that is neither string nor symbol.
  3. that symbols are otherwise inexpressible as first-class values on the wire.

I do not think Agoric needs well-known-symbols or registered symbols on the wire. We can make do well with just one bank of method names and enjoy the reduction in complexity.

@zenhack
Copy link
Collaborator

zenhack commented Jun 26, 2023

I would like to arrive at an agreement that:

I think I agree with these.

These requirements alone preclude representing symbols on the wire #46.

I'm not sure this is true, but I'm coming around to the idea that they may be more trouble than they're worth.

These requirements suggest that we use the inter-language common ground of dromedaryCase method names on the wire.

This matches the capnp convention. I'm not convinced we need to enforce dromedaryCase here, and it is probably more trouble than it's worth, but we should follow this convention for any standard interfaces we define.

I move:

...these are not sticking to my brain; can you elaborate on the design you're proposing?


I've been musing as to whether it wouldn't be better to just have method calls be the only thing supported, and let implementations wanting bare functions use something like python's __call__ convention. While as a functional programmer I'm partial to lambdas in general, it seems like the lack of structure is generating a lot of unwanted complexity, and requiring method names makes it easier to extend an interface with more methods later -- something that isn't as much of a problem with a local program that can just be updated atomically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants