Classes as Sugar -- old threads revisited

Mark S. Miller erights at google.com
Mon Mar 30 19:41:30 PDT 2009


Part 2 of 2


Since a class declaration is like a function declaration, we use a
similar syntax:

  ClassDeclaration:
    "class" Identifier "(" FormalParameterList_opt ")" "{" ObjectBody "}"

In this note, we explain the desugaring of ClassDeclaration via an
intermediate desugaring to ObjectExpression.

  ObjectExpression:
    "object" ("implements" Expression)_opt "{" ObjectBody "}"

where

  class Foo(p1, p2) { ... }

desugars to

  const Foo(p1, p2) {
    return object implements Foo { ... };
  }

which desugars to

  const Foo = Object.freeze(function Foo(p1, p2) {
    return object implements Foo { ... };
  });

As with ConstFunctionDeclaration, if ObjectDeclaration is not accepted
into ES-Harmony, then consider it only an explanatory device.

  ObjectBody:
    Statement
  | Declaration
  | "public" Declaration
  | "public" Identifier (":" Expression)_opt "=" Expression
  | "public" Identifier "(" FormalParameterList_opt ")" "{" FunctionBody "}"

where

  public x :T = y;

desugars to

  public const x :T = y;

and

  public foo(p1, p2) { body; }

desugars to

  public const foo(p1, p2) { body; }

Revisiting Peter's example,

  class Point(privX, privY) {
    let privInstVar = 2;
    const privInstConst = -2;
    public toString() {
      return ('<' + getX() + ',' + getY() + '>');
    };
    public getX() { return privX; };
    public getY() { return privY; };
    public let pubInstVar = 4;
    public pubInstConst = -4;
  }

desugars to

  const Point(privX, privY) {
    return object implements Point {
      let privInstVar = 2;
      const privInstConst = -2;
      public toString() {
        return ('<' + getX() + ',' + getY() + '>');
      };
      public getX() { return privX; };
      public getY() { return privY; };
      public let pubInstVar = 4;
      public pubInstConst = -4;
    };
  });

which desugars to a hoisted

  const Point = Object.freeze(function(privX, privY) {
    return object implements Point {
      let privInstVar = 2;
      const privInstConst = -2;
      public const toString() {
        return ('<' + getX() + ',' + getY() + '>');
      };
      public const getX() { return privX; };
      public const getY() { return privY; };
      public let pubInstVar = 4;
      public pubInstConst = -4;
    };
  });
  Object.freeze(Point.prototype);

The remaining elements needing explanation, ObjectExpression,
ObjectBody, and

  "public" Declaration

desugar together into a LetExpression whose final expression is
created by gathering together representatives of the "public"
declarations. The intent of the "implements" clause is that the value
of the object expression be tagged somehow with an unforgeable nominal
type, such that this value is able to pass the corresponding
guard. For now, I will take a shortcut and assume that when a
guard-value is a function, that the dynamic type-like test is approx

  isFrozenProp(guard, 'prototype') && (specimen instanceof guard)

If the function's 'prototype' property is frozen, then instanceof is
at least a monotonic test. However, it is effectively forgeable -- it
guarantees no useful property -- since anyone may create an object
that passes this test but has arbitrarily weird behavior. (Thanks to
Waldemar for emphasizing this point at our last meeting.) In order to
have a high integrity desugaring of ClassDeclaration or
ObjectExpression, we need better lower level support for some kind of
trademarking mechanism. We will need to revisit this issue, but not in
this note.

With this caveat, our example further desugars to

  const Point = Object.freeze(function(privX, privY) {
    return let {
      // hoisted functions first
      const toString = Object.freeze(function() {
        return ('<' + getX() + ',' + getY() + '>');
      });
      Object.freeze(toString.prototype);
      const getX = Object.freeze(function() { return privX; });
      Object.freeze(getX.prototype);
      const getY = Object.freeze(function() { return privY; });
      Object.freeze(getY.prototype);

      let privInstVar = 2;
      const privInstConst = -2;
      let pubInstVar = 4;
      const pubInstConst = -4;

      Object.freeze(Object.create(Point.prototype, {
        toString: {value: toString},
        getX: {value: getX},
        getY: {value: getY},
        pubInstVar: {get: Object.freeze(function{return pubInstVar;}),
                     enumerable: true},
        pubInstConst: {value: pubInstConst,
                       enumerable: true}
      }))
    };
  });
  Object.freeze(Point.prototype);

Actually, I cheated above. Notice the lack of an "enumerable: true" in
the properties representing toString, getX, and getY. Rather than
consider

  "public" Identifier "(" FormalParameterList_opt ")" "{" FunctionBody "}"

equivalent to

  "public" "const" Identifier ...

consider it instead to be almost identical but representing a method
definition. As a method definition, it makes sense (to me at least) to
suppress its enumerability.

By considering this syntactic form to represent a distinct method
definition production, we can almost cleanly address another of
Waldemar's concerns. Within the FunctionBody of a method production,
we can rename all free "this"s to refer to the object being made. For
example

  object { public getMe() { return this; }}

could desugar to

  let {
    const t1 = object { public getMe() { return t1; }};
    t1
  }

where t1 is a variable name not otherwise used in the
ObjectExpression. This would desugar to

  let {
    const t1 = let {
      const getMe = Object.freeze(function getMe() { return t1; });
      Object.freeze(getMe.prototype);
      Object.freeze(Object.prototype, {
        getMe: {value: getMe}
      })
    }
    t1
  }

The remaining problem left unaddressed by this proposal is that it
creates an unmet need for an analogous private method production,
where "this" is analogously renamed.

-- 
    Cheers,
    --MarkM


More information about the Es-discuss mailing list