-
Notifications
You must be signed in to change notification settings - Fork 9
Description
Recording some of our discussions with @michaelficarra and others from earlier.
The committee feedback was to support string based required fields for legacy compat with existing protocols. Additionally, having a way to use strings for both the required fields and the provided fields seems like it would make adoption much more likely. There is a tension here: one wants to have good enough ergonomics for string-based names to facilitate adoption, but not so good that they will be used over symbols.
It seems that making symbols the default case and requiring additional syntax for strings satisfies that criterion sufficiently that we don't have to introduce additional hoops to make strings even less ergonomic.
I strongly believe that the ergonomics of having to quote every identifier feel too weird, especially in the (I suspect) common case where one wants every field or nearly every field to be a string.
But also, is this even something that the protocol should be defining at all? It seems that the decision of whether to expose strings or not should live in the host class since it affects its API. I can easily see the same protocol being used in both ways by different classes. Basically, the decision is, are you importing logic or both API and logic? That's a host class decision, not a protocol decision.
But if this is specified at the point of implementation, what is the contract? If a property can sometimes be called "bar" and sometimes Foo.bar there is not much of a contract. This can be resolved by having both the symbols and the strings, so that the contract is always symbol-based and the strings are optionally layered over it. This also means that the protocol itself can continue to use the symbols in its own implementations. It also gives the host class better ergonomics for disambiguation (with strings is a placeholder for better syntax):
protocol M1 { foo() {} }
protocol M2 { foo() {} }
class A implements M1 with strings, M2 with strings {}
// Error!protocol M1 { foo() {} }
protocol M2 { foo() {} }
class A implements M1 with strings, M2 with strings {
foo() {
this[M1.foo]();
this[M2.foo]();
}
}These semantics are somewhat similar to Java’s InterfaceName.super. Without it, it's unclear how disambiguation is supposed to work (see #55).
But then, how to do both for actual data properties, such as the required fields? Implementing a protocol could also generate an accessor for this too, unless the class also implements the symbol. E.g. suppose you have two protocols that both use id as a required field:
protocol Loggable {
id;
log() {
// elided
}
}
protocol DBEntry {
id;
}Then we can still provide separate implementations for both:
class Foo implements Loggable with strings, DBEntry with strings {
id = "foo";
[BDEntry.id] = "foobar";
// Generated
get [Loggable.id] () {
return this.id;
}
set [Loggable.id] (value) {
this.id = value;
}
}This also means that when a protocol is implemented this way, classes get a choice of satisfying its requirements by either specifying the symbol or the string. This is useful as it is common to satisfy a protocol via a symbol but want to import provided members as strings. E.g.
protocol ToString {
tag;
toString() {
return `[object ${this[ToString.tag]}]`;
}
}
class Foo implements ToString with strings {
[ToString.tag] = "Foo";
}
(new Foo) + "" // "[object Foo]"I think something like this is the missing piece to make protocols a complete replacement for pretty much every partial/mixin/trait proposal out there.