tc39 / proposal-private-fields

A Private Fields Proposal for ECMAScript
https://tc39.github.io/proposal-private-fields/
318 stars 19 forks source link

Hard-private vs soft-private #33

Closed littledan closed 7 years ago

littledan commented 8 years ago

Should private state have an "escape hatch" like TypeScript private, or be strongly private the way that closed-over values are? Soft-private isn't fully written out the way that @zenparsing has done an excellent job on the current WeakMap-based proposal, but the core would be that "private state" is simply (public) symbol-named properties, with syntactic sugar for those symbols, and possibly some kind of introspection over them. It might be bad for overall language complexity/intelligibility to have both kinds of private state in the language and better to decide up-front what we want. Below, I @-mention people who either made this argument or who I think might be especially interested in the argument.

Soft-private

Soft-private would be syntactic sugar for symbols. In ES6, you can currently write code like this:

let x = Symbol("x");
class Foo {
  constructor() {
    this[x] = 1;
  }
}

With a soft-private language feature, there would be syntactic sugar that looks like this:

class Foo {
  #x = 1;
}

Advantages

Hard-private (the proposal described in this repository currently) is based on state that can really only be accessed by code within the class body. This has been constant across all of the different proposals that have been advanced to TC39, though not what TypeScript provides.

Advantages

let get, set;
class Foo {
  #bar
  static {
    get = x => x.#bar;
    set = (x, y) => x.#bar = y;
  }
}

Even without static blocks, you could create a static method which does the exporting, though this is ugly.

Thoughts?

ljharb commented 8 years ago

Hard-private is fully polyfillable via WeakMap, and I think that the encapsulation gained from hard-privacy is critical, and think the benefit of hard-privacy utterly trumps, thousands of times over, any ability to polyfill in browsers that don't ship WeakMap.

Private state shouldn't be tested or accessible. If you need to test it, it shouldn't be private, and vice versa.

The "new language construct" here is simply exposing a way for users to create internal slots - something that previously only the spec was empowered to do. imo this simplifies understanding and reduces complexity.

glen-84 commented 8 years ago

I apologize if this is out of place, but would there be value in supporting both?

Use hard-private (#x):

Use soft-private (private x):

Edit: It would also be compatible with TypeScript, if I'm not mistaken.

ljharb commented 8 years ago

That seems a bit confusing, and "soft private" doesn't require additional syntax - you'd just use module-level Symbols as normal public properties.

wycats commented 8 years ago

@littledan I think you covered most of my thoughts on this topic pretty well.

A few additional thoughts:


Fundamentally, "hard private" state means private state on an object that cannot be seen by any objects not expressly granted access to that state. This means that requests for that state cannot be intercepted by installing a proxy, nor by creating a subclass of the original class.

In contrast, "soft private" state means private state on an object that, while difficult (or annoying) to access, can be accessed without an explicit grant. Any attempts to "secure" this kind of state can't possibly work (tautologically).

Most of the proposals for private state, including the one in this repository, work by using lexical scope to grant access to the private names, an elegant approach that eliminates the possibility of snooping through proxies and provides a nice, static semantics that is useful for efficient implementations.

A minor concern: from this perspective, any plausible semantics for protected state must be considered soft-private, since it is necessarily possible to capture a "soft-private" symbol by constructing a subclass:

// this example uses theoretical syntax, but the basic point applies to protected syntax
// modelled in terms of the current hard-private state proposal.

class Parent {
  protected #mine;
}

// in a module attempting to gain access to #mine

class Child extends Parent {
  get mine() { return o => o.#mine; }
}

let mine = new Child().mine; // I now have a generic way to access Parent's #mine
mine(new Parent()); // using it

This means that if we are ever to add support for protected state, we will end up with soft-private state regardless. Given that protected state is highly desired by some members of the committee, starting with hard-private may result in getting both anyway, and I would prefer not to do a piecemeal approach that could end up less coherent than we would like.

(As an example, if we end up modelling protected state on hard-private state, protected state will end up with the same proxy blindness as hard-private state. But given that protected state has no special security reason for this limitation, this would be an unnecessary limitation that could feel incoherent.)


@ljharb said:

The "new language construct" here is simply exposing a way for users to create internal slots - something that previously only the spec was empowered to do.

@littledan said:

This is something which, to be efficient, probably has to be built into the language, unlike soft-private which can be done by a transpiler like TypeScript.

I find these arguments compelling.

A possible approach that could allow us to make progress without making an immediate decision on scarce syntax might be to add support for private state via a meta-property.

class Person {
  private.first;
  private.last;

  fullName() {
    return `${private.first.get(this)} ${private.last.get(this)}`
  }
}

As people have pointed out, this would give transpilers enough core semantics to transpile both hard-private and soft-private immediately, without needing to make an immediate call about which of the two styles we want to grant the most sugary syntax (#foo) to.

If transpilers can efficiently experiment with both styles, this will teach us whether one of the two styles is dominant and whether protected is highly desired (and what its semantics might be). We might also learn that hard-private and soft-private are both very popular, which might mean that we should try to find sugary syntaxes for each.

The nice thing about a meta-property is that it gives us the new language feature that we need for efficient transpilation, putting it on equal footing with symbols both from @littledan's perspective (ability to experiment efficiently using transpilers) and @ljharb's (exposing a feature of spec objects to user-space), decoupling it from the most sugary syntax.

littledan commented 8 years ago

To state my own position: I think we should stick with hard-private. I think it's really valuable to allow programmers to build solid abstractions, just like the platform. This gives library authors more ability to evolve over time, without users depending on implementation details. It's just good object oriented design to have a reliable separation between interface and implementation. As language designers, I think it's OK for us to take an opinionated stance in favor of this sort of good design. When library authors do want to expose things as inconvenient/soft-private/protected parts of the interface, there are currently other mechanisms to do so.

Current mechanisms for soft private state

ES currently has two mechanisms to provide soft-private state:

I'm not sure I'm convinced that these two existing mechanisms are insufficient as they currently stand. Any soft-private mechanism would, at its core, have to give you a nice way to access and distribute the symbols, but I have a hard time picturing anything nicer than our current support for symbols in lexically scope variables. Square brackets being ugly might be the main disadvantage, since you have to put them both before and after the symbol name, and because it looks like array/object-as-a-map reference, which might not meet some programmers' mental models. Maybe we could have some syntactic sugar just for this purpose (@@? ..?).

Avoiding using a sigil like #

To iterate slightly on Yehuda's private-state-without-a-sigil proposal, if we want something for private state that won't take up much syntactic space, we could use syntax where basically private. replaces # everywhere. That avoids the thorny question of what private.first would be (an object? what if you change it?) As an example:

class Point {
  private.x = 0;
  change(newX) { this.private.x = newX; }
  retrieve() { return this.private.x; }
}

We could also have private.x as syntactic sugar for this.private.x. This proposal avoids the use of '#', which we might want to save for later, while allowing implementations to do all of the same things that they would do with the currently specified proposal. The spec change would be just in the syntax.

wycats commented 8 years ago

Naming conventions like using an underscore at the start or end of a name. This is the most common. The main hazard is that it's not inconvenient enough, so people might circumvent it more often than library authors would like.

That isn't the main issue. The main issue is that the underscore namespace just becomes another contended namespace. Using an underscore in a subclass means verifying that the superclass doesn't use the same name (or trying to use a name that is unlikely to collide). Adding an underscore property in a class that is meant to be subclassed is therefore a semver-breaking change. In practice, this is sufficiently problematic to cause serious problems in my experience.

Symbols are pretty well articulated for this purpose: we have computed property names which make them more ergonomic to use, so you can use them everywhere a string property key can.

We've been using this style quite a bit in Ember, and I've also been using the TypeScript support for private fields. As a point of information, the lack of good "friend" support in TypeScript causes me to lean on the "soft" nature of the TypeScript approach.

I strongly prefer the TypeScript style to the symbol style, which requires a noisy outside-of-class declaration. It's possible to think of the outside-of-class declarations as "declarations", but it just feels like too much busy-work in practice. (I do it when I'm not in TypeScript because of the problems with underscores, but it's more annoying).


I'm not sure I'm convinced that these two existing mechanisms are insufficient as they currently stand.

I can only speak for my own experience: the underscore mechanism is unusable in many cases, and the symbol mechanism exceeds my ceremony tolerance in many cases. What this means is that it's much easier to be sloppy (and use public fields) than use symbols.

That said, I agree that many of these claims are hard to verify, and I also agree that it's possible to transpile various versions of the soft-private-using-symbols approach. I'd like to attempt that kind of experimentation before settling, as a language, on using # for hard-private.

Since the primary rationale for landing hard-private now (as opposed to experimentation via transpilation) is exposing a language feature that can be efficiently implemented, I argue that we should prefer a less-sugary version of the hard-private feature. That way, we can experiment with sugar-for-hard-private and sugar-for-soft-private via transpilation, and determine which of our various instincts reflects how people will use the feature.

I also think, in general, it makes sense for us to be more cautious about rushing language features that can be transpiled, and instead encourage experimentation via transpilation (as you have astutely argued for re: decorators).

wycats commented 8 years ago

To iterate slightly on Yehuda's private-state-without-a-sigil proposal, if we want something for private state that won't take up much syntactic space, we could use syntax where basically private. replaces # everywhere.

I'm fine with that. You could easily construct the object yourself if you wanted to share it (and perhaps with decorators, you could make it easy to do so):

class {
  private.first;

  get firstAccessor() {
    return o => o.private.first;
  }
}
littledan commented 8 years ago

Which part of the symbol mechanism is excessive ceremony for you? Is it having to declare the variable, use square brackets, or both? Do you have another idea for how you'd like to share the symbol around?

glen-84 commented 8 years ago

That seems a bit confusing

Why? They would each have a different name and purpose (private fields/data/whatever vs private properties).

and "soft private" doesn't require additional syntax - you'd just use module-level Symbols as normal public properties.

So now we must define all of our property names outside of the class? That's crazy, and I think that is what @wycats was referring to as being "a noisy outside-of-class declaration" and exceeding his ceremony tolerance. I agree 100%.

If symbols are to be used for soft-private state, they need some sugar, and the TypeScript-like syntax is already well-known and commonly used.

Regarding hard-private, I think that the private.var syntax might be confusing, and appear as either a typo (period instead of space) or a property access on some user-defined object. Perhaps private#var would make more sense in this case. Both options would allow for other access levels (assuming that is technically possible), which are not possible with the current #-prefix approach.

wycats commented 8 years ago

@littledan @tc39/delegates We had a very brief discussion (10 minutes) at the last meeting about private state, and at the time I wasn't comfortable advancing this feature without a longer discussion about soft vs. hard private.

After the meeting, I had a conversation with Mark Miller and @allenwb about hard vs. soft private (especially as they relate to protected and friendly fields), and I think we came up with a path for all of these features that can be based on the private field approach in this spec.

In short, the idea is that names could be "imported" into a class, either from a superclass or a friendly class. Here is some straw man syntax as an example:

const Friends = Symbol();

class LifeForm {
  protected #name;
  #homePlanet;

  export #homePlanet for Friends;
}

class Human extends LifeForm {
  use protected #name;
}

class Spaceship {
  use #homePlanet using Friends;

  warp(lifeForm) {
    setACourse(lifeForm.#homePlanet);
  }
}

This syntax and semantics is still awkward and would want quite a bit of refinement, but it shows that providing a mechanism for exposing (and using) a symbol from a superclass, as well as collaboration between friends, could be accomplished within the rubric of this proposal.

I think that protected (and support for some kind of friend access without hacks) is important, and something that we should try to work out (or reject, perhaps) as we advance this proposal.

In light of this discussion (which we couldn't have during the meeting itself because we were restricted to 10 minutes), I am comfortable advancing the feature to Stage 2. (If I recall correctly, there were several other committee members who also seemed uncomfortable advancing given the short timeframe for discussion, but we should be able to move this fast in September.)

littledan commented 8 years ago

I'm all for figuring out how to make friends with better forms of access. I'll try to iterate on this.

syrnick commented 8 years ago

I'd like to make an argument for soft private semantics. In my experience I had to use private state several times to debug or patch critical issues.

In the case of open source libraries, the user has the option of forking the library and removing the hard private declarations. It’s just a hassle.

Hard private makes it difficult to patch bugs for a specific environment that the library maintainer didn’t quite envision. Those issues could be hard to address properly and might require major refactoring that the end user isn't capable of doing.

Reaching out into private methods should definitely be discouraged. Library maintainers are NOT expected to keep private variables the same over patch releases or even within a patch release.

The user should be given static analysis tools bring the code into compliance as much as possible and in rare cases opt out where strictly necessary.

I think it would be fine if accessing soft private properties required some ugly syntax like foo.__private_property__bar ala React's dangerouslySetInnerHTML. That would make it obvious that this is code is suspect.

glen-84 commented 8 years ago

foo.__private_property__bar

I hate these magical properties ... couldn't there just be some from of reflection function/class?

littledan commented 8 years ago

@syrnick Given that a library author can already do foo.__private_property__bar, what kind of language feature do you think would make sense to make private state easier to use, but still a hassle to get into? Where's the right "hassle line" exactly, if forking a library is too hard, but foo.bar_ might be too easy (with shadowing from subclasses, using as a public interface, etc)?

dalexander01 commented 8 years ago

I absolutely prefer private.field or this.private.field to #field or this#field.

Is the sigi-less option suggested by @wycats still an option?

littledan commented 8 years ago

@dalexander01 Which sigil-less option--using private. instead of # as the sigil? Yes, that's still possible. Using foo.bar for private state access? I don't see how that's possible, but I'm open to further proposals.

dalexander01 commented 8 years ago

@littledan yep, private.field is what I had in mind.

littledan commented 8 years ago

Oops, @bakkot pointed out to me that this idea makes no sense actually--private is of course a valid property name. We could do something ugly like private(receiver).field but not receiver.private.field.

syrnick commented 8 years ago

@littledan the owner of the private properties would access them normally (e.g. via this.foo or this.private.foo). It could be just foo if that can be properly interpreted when foo statically resolves to a private property.

@glen-84 that's a perfect reaction. Any "trespassing" should raise an alarm to the reader.

I think it's ok to reserve "private" in classes that want to use private properties. Only if the class (or a base class) declares a private property, it would be able to access private properties. E.g.

class A {
    private foo;
    getFoo() {
        return foo;
    }
    getFoo2(anotherInstanceOfA) {
        return private(anotherInstanceOfA).foo;
    }
}
ljharb commented 8 years ago

@syrnick would private(obj) be a syntax error, or would it return an object containing private field entries? I'd hope the former.

zenparsing commented 8 years ago

@syrnick Consider also the interaction with public class properties. With your proposal:

class C {
  private foo = 1;
  bar = 2;

  method() {
    this.bar; // 2
    this.foo; // undefined - wth? 
  }
}

The sigil-based proposal works elegantly with all of these constraints:

class C {
  @foo = 1;
  bar = 2;

  method() {
    this.bar; // 2
    this.@foo; // 1 - as expected
  }
}
amiller-gh commented 8 years ago

@littledan, wouldn't the private(receiver) syntax also break the internet? private is a valid variable name (sadly).

Consider:

var private = "SECRET STRING – DON'T TELL!";

class Foo {
  private secret;
  method(){
    // What is `private` here?!
  }
}

Either a) private ignores normal context lookup (like arguments) and always uses the built-in, potentially breaking existing sites, or b) defers to a private value in the context lookup if it exists, meaning a framework can break the world by doing window.private = 'lol'

In the other issue thread there seemed to be excitement about the -> accessor instead of the # sigil, thought I'd pull it in here as well to make sure its part of the discussion: https://github.com/tc39/proposal-private-fields/issues/14#issuecomment-243684090

Ltrlg commented 8 years ago

AFAICT, this is not an issue since private is not a valid variable name in this context:

  1. private is a reserved word in strict mode
  2. class definitions are always strict mode code
amiller-gh commented 8 years ago

Well would you look at that, you're right. Ignore that I said anything then!

indolering commented 7 years ago
class C {
  private foo = 1;
  bar = 2;

  method() {
    this.bar; // 2
    this.foo; // undefined - wth? 
  }
}

That is, at most, a one off mistake.

indolering commented 7 years ago

WRT testing: there is an advantage to allowing access to private fields. Yes, you can theoretically test every code path but it's a lot of extra work. I'm pretty ignorant to how these features are actually implemented, but would it be possible to allow access based on a runtime switch or a macro so we could enable access during testing?

ljharb commented 7 years ago

If you need to test it, it shouldn't be private.

esprehn commented 7 years ago

I'm not sure that's universally agreed upon. For example it's very common in C++ to friend the Test class so it can access methods and state you would otherwise not want used. Having a hard private without an escape hatch like "friend" or a reflection API is unusual across most languages. I'd certainly prefer we have some escape hatch, even if it's ugly, since there's many existence proofs of various use cases which need it across many languages and systems.

ljharb commented 7 years ago

All of those use cases can use Symbols, right now, for soft-private.

indolering commented 7 years ago

If you need to test it, it shouldn't be private.

That's not what I was asking, would a runtime override (which is pretty easy to do when using Mocha or Selenium) be a nonsensical proposal from an implementation point of view?

Haxe has a an access modifier that allowed access from my testing libraries. This was very convenient when testing private methods that handled disambiguation of different input types. It certainly made my code cleaner and reduced clutter on the API. I would think that a runtime switch would prevent the abuse of a Reflection API outside of a test framework like Mocha or Selenium.

bakkot commented 7 years ago

Certainly e.g. v8 could expose a native method to access private fields, depending on its implementation and whether it chose to, and a node library could make use of it. That would be outside the purview of this proposal or TC39.

dalexander01 commented 7 years ago

If you need to test it, it shouldn't be private.

This can not be overstated enough. Reflection, friend access, and all of that are just ugly workarounds where refactoring hard to test private logic into reusable modular public classes or functions would be a much better solution.

littledan commented 7 years ago

I think hard private is a better default. In DECORATORS.md, I provide an explainer for how users can provide more visibility if they want it for particular fields.

syrnick commented 7 years ago

I'm not sure what's the benefit of hard private for library users.

Hard-private could provide actual guarantees about visibility and more assurances to library authors that their internal state will not be accessed inappropriately, allowing them to build something more like builtin internal slots.

Library author should be happy with guarantees about state access conditioned on the internal state not being used. If a user wants to violate the supported interface, the library author isn't responsible at all.

The users should need a very good reason to access private state. I think I currently have ~10 places where I need to do that. I'm aware they are a ticking time bomb.

If you need to test it, it shouldn't be private.

That might be fair for the tests you write for your own code.

When I write integration tests and I need to simulate edge case failures, I sometimes need to have access to a private property of a module that I don't maintain. If my tests break because the library changed its implementation, that's awesome! I might have to inspect the changes.

littledan commented 7 years ago

I think of the benefit of hard-private as more on the side of the person exporting an interface than the person using it. People who write libraries don't want to break their users! Hard-private gives library authors a way to write code that they can be sure their users are not depending on. Users then, indirectly, benefit from the fact that the libraries can be better-maintained as a result.

ljharb commented 7 years ago

@syrnick the library author absolutely is responsible for anything their users do - saying "please don't do this" is irrelevant. If it's possible, then it's supported and public. I agree that you should need a very good reason to access private state - and if it's good enough, the private state should be made public (which is synonymous with "accessible"). If not, hard private by default is how you, the author, ensure that the user actually can not depend on something without having a good reason.

syrnick commented 7 years ago

@ljharb I have to disagree with that. The library author can only maintain and reason about their public interface in decisions on how to proceed with development. There's no way of knowing how people actually use your software, it's only important that you stick to semver rules w.r.t. your public interfaces. That is hard enough and your users should trust you on exactly that.

If it's possible, it doesn't mean it's supported. In the extreme case, people will run on different versions v8/node/chrome/edge, use transpilers and limited embedded interpreters. It may work or it may not. Unless you expressly declare support for something, it's safe to assume it won't work.

ljharb commented 7 years ago

@syrnick all parts of your interface that are accessible are part of your public interface - that's the point. "hard private", whether via this proposal, WeakMaps, or closures, is the only way to reduce the size of your public interface.

syrnick commented 7 years ago

all parts of your interface that are accessible are part of your public interface

I disagree. There's plenty of ways to define a public interface and once it's defined, that's the public interface. E.g. https://nodejs.org/api/dns.html is the public interface regardless of what one might actually access there.

I would love to see a version that makes accessing private state very ugly and obvious. I have no doubt that should be discouraged and frowned upon.

indolering commented 7 years ago

Private state is not really guaranteed private, so no hard guarantees about properties that users may expect if they want security guarantees about how their objects are used

What "security guarantees" might one derive from this? I would think that most actual uses would still leak private data....

indolering commented 7 years ago

@littledan Prefixing variable names with underscores and anything that requires a transpiler does not count* a serious alternative to private members.

@syrnick I don't think you are going to convince anyone that abuse of internal APIs is a good idea. I only suggested allowing inspection of internal APIs in test environments because it can't be abused to avoid a refactor. That being said, I don't think library authors being able to protect their internal API is a huge plus. If someone does break that barrier, then they deserve to have their software broken.

Edit: added count*

bakkot commented 7 years ago

@syrnick, @indolering, library authors do not generally consider themselves free to break their user's pages and applications just because those users were depending upon some part of the library's interface[1] which the library author did not intend them to depend upon, no matter how much those users might in your view deserve it. I think this is the correct attitude for them to take.

I don't think you're going to have much luck trying to convince the committee otherwise.

[1] Footnote: if you're using a definition of "interface" which excludes some accessible parts of a library's exported classes and objects, then imagine I used some other word here which meant "the accessible parts of a library's exported classes and objects".

syrnick commented 7 years ago

Have anyone done a write-up on how this compares with other languages? If the intention is to have a language-level spec that access to private state is not allowed, would this be the strictest language of all major languages?

I haven't verified all of these: https://rosettacode.org/wiki/Break_OO_privacy - looks like most languages have a way.

I don't know Erlang; this http://stackoverflow.com/questions/15433565/erlang-testing-non-exported-private-function-of-module-using-common-test claims that private methods are hard private; don't know about data.

I am worried about hitting an uncaught exception in my browser and having no way of figuring out what's going on, because all the state is hard private.

Perhaps, I don't understand whether hard private proposal allows any way of programmatically accessing the private state, whether it's allowed as a runtime option or the goal is to have really none of that.

zenparsing commented 7 years ago

There is nothing in this proposal that would prevent a console or debugger from displaying the contents of private fields.

I believe that the Chrome console displays the internal slots for Promise instances, for example.

ljharb commented 7 years ago

@syrnick yes, the goal is to have zero possible way, from the normal JS runtime, to access private state (the debugger, of course, is not the normal JS runtime). "Private" needs to mean exactly that.

indolering commented 7 years ago

@bakkot @ljharb I largely agree with you, I was just trying to mediate.

aluanhaddad commented 7 years ago

Have anyone done a write-up on how this compares with other languages? If the intention is to have a language-level spec that access to private state is not allowed, would this be the strictest language of all major languages?

Are suggesting that JavaScript should introduce but deliberately not enforce a concept of privacy because most other languages in wide use have historically done the same?

While it would need to be examined on a case by case basis, I very much doubt that most of these languages aspired to the goal of non-enforcement. Practical technical limitations, legacy dept, and interop constraints all seem as if not more plausible reasons.

Even if it were true that each of these languages introduced privacy; a notion which implies massive overhead in terms of tooling, code analysis, theoretical basis, language evolution, and last but not least cognitive load; with the express intent of making it vacuous it does not imply that JavaScript should do the same.

indolering commented 7 years ago

@aluanhaddad I think most people would agree that popularity is only one indicator of quality. But lets try to keep this thread focused on relative merits of hard/soft private and not on language design philosophy. I'm worried that this will just lead to a rather non-productive back-and-forth....

aluanhaddad commented 7 years ago

Fair enough, I did not mean to take it off topic, I just wanted to state that precedent is not sufficient and may not be desirable.

Regardless, I think hard private is the way to go.

wycats commented 7 years ago

While it would need to be examined on a case by case basis, I very much doubt that most of these languages aspired to the goal of non-enforcement.

Ruby has internal fields ("instance variables") that are hidden by default (only methods are considered a part of the public interface in Ruby). It is possible to access instance variables from outside of a class by using the reflection method instance_variable_get.

Adding the reflection method was surely intentional.

In Ruby, using this facility is relatively rare, but it addresses a pretty important limitation in the design of extensible object oriented abstractions: it's very difficult to design, a priori, the perfect set of extensibility points. It is also far easier to advocate that maintainers add a new extension point by demonstrating popular functionality that would benefit from the use-case.

Early on in the lifetime of a new library, it can be very beneficial to leave these kinds of escape valves available so that people can experiment with functionality that wouldn't be possible with the public API. Over time, as the abstraction gains a more complete set of extension points (and as compatibility constraints become more acute), it makes sense to more aggressively lock down these escape valves to gain better control over the complete surface area of the abstraction.

Of course not everyone will agree with this strategy, but having seen it work well in practice, I think it's wrong to dismiss it as an accidental artifact that people in other languages would broadly agree was a mistake.