Open gibson042 opened 1 year ago
It might be nice if property access and function invocation were orthogonal, though I know this isn't how js works wrt. this
.
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?
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).
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").
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?
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:
I wish to then convince you:
dromedaryCase
method names on the wire.I move:
dromedaryCase
.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.
I would like to arrive at an agreement that:
I think I agree with these.
These requirements alone preclude representing symbols on the wire https://github.com/ocapn/ocapn/issues/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.
_Originally posted by @gibson042 in https://github.com/ocapn/ocapn/pull/42#discussion_r1201030763_
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]".