eslisp icon indicating copy to clipboard operation
eslisp copied to clipboard

The `object` macro is not ES6-friendly

Open anko opened this issue 10 years ago • 30 comments

ES6 introduces dynamic property names in object expressions:

var x = 42;
var obj = {
  [ 'prop_' + x ]: 42
};

At the moment, eslisp's object macro can't unambiguously accommodate that. Given that (object a b) compiles to { a : b }, what should compile to { [a] : b }?

Similarly to previously in #13, this can't simply be solved by having (object "a" b) compile to { a : b } instead and (object a b) to { [a] : b }, because it must continue to be possible to express both { a : b } and { "a" : b } for stuff like Google's closure compiler, and for when it's necessary to ensure that part of the code is also valid JSON.

anko avatar Sep 28 '15 23:09 anko

Valid JSON can be done separately via a (json) function; that can limit values to valid JSON values, and invoke a "jsonify" contract for values that aren't valid JSON.

tabatkins avatar Sep 30 '15 19:09 tabatkins

@impinball brought up another issue in the chat:

things like {a, [b + c]: d}. Using (object a (+ b c) d) would be ambiguous.

In total, there are 8 cases in ES6:

  • regular properties: {a: b}
  • regular properties w/ string key: {"a": b}
  • computed properties: {[a]: b}
  • shorthand: {a}
  • getter/setter: {get a() {}, set a() {}}
  • getter/setter w/ string key: {get "a"() {}, set "a"() {}}
  • method: {a() {}}
  • method w/ string key: {"a"() {}}

lhorie avatar Oct 05 '15 13:10 lhorie

Yep. I would like to mention that the shorthand method can be done without, and in many different compile-to-JS languages, and even in Lua (another prototype-based language), that functionality simply doesn't exist. [1]

That still leaves 4 different versions to cover, including all permutations thereof.

  • regular properties: {a: b}
  • computed properties: {[a]: b}
  • shorthand properties: {a}
  • getter/setter: {get a() {}, set a() {}}

[1] Well, I kinda fibbed a little with ClojureScript, mainly with regards to (defprotocol).

dead-claudia avatar Oct 05 '15 14:10 dead-claudia

Oh, looks like I missed a bunch other cases:

  • {* a() {}} (generator method shorthand)
  • {* "a"() {}} (generator method shorthand w/ string key)
  • {[a]() {}} (computed method)
  • {get [a]() {}} (computed getter/setter)
  • {* [a]() {}} (computed generator method shorthand)

Note that, unlike shorthand syntax ({a}), method shorthands ({a() {}}) are functionally different from {a: function() {}}. (Notably, they are not constructable). See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions

lhorie avatar Oct 05 '15 14:10 lhorie

There are a lot of functions similar to this in Lisp, where you have a bunch of key/value pairs, some of which can have different syntax. They all work by taking a list of lists, like:

(object ('a 1) ('b 2))
===>
{a: 1, b: 2}

This sort of syntax, while it requires a little more typing than the "just pair them off" plist-inspired syntax that (object) currently uses, is unambiguous and allows for all the object-literal variants:

(object
  ('a)
  ('b 1)
  ((+ c "foo") 2)
  (get 'd (lambda () (return 3))))
===>
{a, b:1, [c+"foo"]:2, get d() {return 3}}

tabatkins avatar Oct 06 '15 18:10 tabatkins

That looks way better.

On Tue, Oct 6, 2015, 14:14 Tab Atkins Jr. [email protected] wrote:

There are a lot of functions similar to this in Lisp, where you have a bunch of key/value pairs, some of which can have different syntax. They all work by taking a list of lists, like:

(object ('a 1) ('b 2)) ===> {a: 1, b: 2}

This sort of syntax, while it requires a little more typing than the "just pair them off" plist-inspired syntax that (object) currently uses, is unambiguous and allows for all the object-literal variants:

(object ('a) ('b 1) ((+ c "foo") 2) (get 'd (lambda () (return 3)))) ===> {a, b:1, [c+"foo"]:2, get d() {return 3}}

— Reply to this email directly or view it on GitHub https://github.com/anko/eslisp/issues/23#issuecomment-145951386.

dead-claudia avatar Oct 06 '15 20:10 dead-claudia

There are a lot of functions similar to this in Lisp

Yeah, let form comes to mind. Speaking of which, I realized that var has the same issue (i.e. can't express var a, b;), so it would also need to change to (var (a 1) (b)) => var a = 1, b in order to support multi-variable declarations.

lhorie avatar Oct 07 '15 14:10 lhorie

It doesn't need to, necessarily; leaving out the value is equivalent to setting it to undefined. But it is probably good to be consistent?

tabatkins avatar Oct 07 '15 17:10 tabatkins

@tabatkins For multiple declarations, it probably is (see my initial comment on the object shorthand ambiguity). For a single declaration, I would love to see this as a shorthand for a common case:

(var a 1) ;=> var a = 1
(var a) ;=> var a
;; (let ...), (const ...)

dead-claudia avatar Oct 07 '15 19:10 dead-claudia

@impinball Ambiguity: Does (var a b) compile to var a = b; or var a, b;?

anko avatar Oct 07 '15 19:10 anko

@anko I was initially thinking of that, but I decided to not bring it up in the first place, as I already knew about the ambiguity. I was also specifically talking about single declarations, where var a, b is literally two separate declarations in one statement.

Or to more directly answer your question, that would compile to var a = b. Think of (var a 1) as a shorthand for (var (a 1)), without nested parentheses.

dead-claudia avatar Oct 07 '15 19:10 dead-claudia

Ambiguity: Does (var a b) compile to var a = b; or var a, b;?

Yes, that was the ambiguity that I was trying to raise.

Personally, I don't really like the idea of special casing (var a 1), because it becomes an extra edge case to deal w/ if you're writing an AST visitor

Also, it creates inconsistent indentation rules:

;this is weird
(var a 1
    (b)
     c 2)
(var (d)
      e 3)

;this feels more idiomatic
(var (a 1)
     (b)
     (c 2))
(var (d)
     (e 3))

But then again, indentation rules can always be enforced at a styleguide level

In any case, I think it makes sense to standardize on having each object property /variable declarator in separate forms. ES6 classes will similarly require special subforms as well.

(var (a)
     (b 1))
(object
  (a)
  (b 1))
(class x
  (a ())
  (b ()))

lhorie avatar Oct 07 '15 20:10 lhorie

I get what you mean. I've just always preferred single declarations instead of multiple in my projects, particularly for initialized variables (less diff noise).

  var a = 1,
-     b = 2,
-     c = 3;
+     b = 2;

Just personal preference. I know the other is more idiomatic for Lisp dialects, though.

(let [a 1
      b 2]
  (+ a b))

dead-claudia avatar Oct 08 '15 02:10 dead-claudia

Edit: added shorthand. Edit 2: edited member expressions per my suggestion in #13.

I have an idea for solving the object dilemma:

  1. Make 'name be a literal key, and name be a computed key.

  2. Make static keys for object as follows:

    (object
      ('key1 value1)
      ('key2 value2))
    
    // In JS:
    {key1: value1, key2: value2}
    

    The preceding quote is idiomatic in Common Lisp, and a similar preceding colon in Clojure.

    This was @tabatkins' idea.

  3. Omitting the value is the property shorthand.

    (object ('prop))
    (object ('prop prop))
    

    Thanks, @tabatkins for catching this.

  4. For computed keys, omit the preceding quote.

    (object
      ((. Symbol 'toStringTag) "MyObject")
      ((foo) "bar"))
    
    // In JS:
    {[Symbol.toStringTag]: "MyObject", [foo()]: "bar"}
    

    These otherwise carry the same semantics as static keys (e.g. (object ((+ "foo" "bar"))) is equivalent to (object ((+ "foo" "bar") undefined)), etc.). As a side effect, (object (foo bar)) qould translate to {[foo]: bar}.

    This was also @tabatkins' idea.

  5. For getters/setters, use (get 'key () ...body) and (set 'key (arg) ...body), respectively. Use the same semantics for the key as with regular properties.

    (object
      (get 'foo () (return (. this _foo)))
      (set 'foo (arg) (= (. this _foo) arg)))
    
    // In JS
    {
      get foo() { return this._foo },
      set foo(arg) { this._foo = arg },
    }
    
  6. For methods, use the following syntax:

    (object
      ('foo () (return 1))
      ('bar (arg) (return arg)))
    
    // In JS:
    {
      foo() { return 1 },
      bar(arg) { return arg },
    }
    

    This might initially seem ambiguous with normal properties, but those can only have two parts. This has three or more, and the second part can only possibly be a list. As long as these invariants are satisfied, there is no ambiguity.

  7. For generators, precede the method with a star. The syntax is otherwise identical to the method syntax.

    (object
      (* 'foo () (yield 1))
      (* 'bar (arg) (yield arg)))
    
    // In JS:
    {
      *foo() { yield 1 },
      *bar(arg) { yield arg },
    }
    

To show an example with all of them:

(const wm (new WeakMap))
(const syms (require "./symbols")

(object
  ('prop)
  ('_foo 1)
  ((. Symbol 'toStringTag) "Foo")

  (get 'foo ()
    (return (. this _foo)))

  (set 'foo (value)
    (= (. this _foo) value))

  (get (. syms 'Sym) ()
    (return ((. wm get) this)))

  (set (. syms 'Sym) (value)
    ((. wm set) this value))

  ('printFoo ()
    ((. console log) (. this foo)))

  ('concatFoo (arg)
    (return (+ (. this foo) arg)))

  (* 'values ()
    (yield (. this foo)))

  (* 'valuesConcat (value)
    (yield (. this foo))
    (yield value)))
// In JS:
const wm = new WeakMap()
const syms = require("./symbols")

{
  prop,
  _foo: 1,
  [Symbol.toStringTag]: "Foo",

  get foo() {
    return this._foo
  },

  set foo(value) {
    this._foo = value
  },

  get [syms.Sym]() {
    return wm.get(this)
  },

  set [syms.Sym](value) {
    return wm.set(this, value)
  },

  printFoo() {
    console.log(this.foo)
  },

  concatFoo(value) {
    return this.foo + value
  },

  *values() {
    yield this.foo
  },

  *valuesConcat(value) {
    yield this.foo
    yield value
  },
}

And let var and friends be like this:

(var (a))         ; var a;
(var (a b))       ; var a = b;
(var (a b) (c d)) ; var a = b, c = d;

(let (a))         ; let a;
(let (a b))       ; let a = b;
(let (a b) (c d)) ; let a = b, c = d;

(const (a))         ; const a;
(const (a b))       ; const a = b;
(const (a b) (c d)) ; const a = b, c = d;

@anko @tabatkins @lhorie What do you all think?

dead-claudia avatar Oct 08 '15 23:10 dead-claudia

Looks good overall, but your 1-value syntax is wrong. We want it to match {a}, which is equivalent to {"a": a}. So the 1-value syntax should only allow actual variables, like (object ('a)). Having a computed key is an error.

So to summarize:

  • 1 value: must be a symbol, like (object ('a)) => {a}.
  • 2 value: desugars to normal property, like (object ('a b)) => {a: b}, or (object (a b)) => {[a]: b}.
  • 3 value: desugars to method syntax, like (object ('a (b) c)) => {a(b) { c }}
  • 4 value: desugars to get/set/generator, depending on whether the first value is get, set, or *

tabatkins avatar Oct 08 '15 23:10 tabatkins

@tabatkins

  1. I overlooked that. I fixed my initial comment to use that instead.
  2. Correct.
  3. Correct.
  4. Correct.

dead-claudia avatar Oct 08 '15 23:10 dead-claudia

Also, are you all okay with the implicit lambda?

dead-claudia avatar Oct 08 '15 23:10 dead-claudia

Yeah, I got no problems with implicit lambda.

tabatkins avatar Oct 09 '15 00:10 tabatkins

Looks very similar to what I currently have in that toy compiler I've been working on, except that I use a special form for computed keys instead of non-computed keys.

For comparison, here's what some of my tests look like right now:

//variable declarations
test('(var (a 1))', 'var a = 1;')
test('(var (a 1) (b 2))', 'var a = 1, b = 2;')
test('(var (a))', 'var a;')
test('(var (a) (b))', 'var a, b;')
test('(let (a 1))', 'let a = 1;')
test('(let (a 1) (b 2))', 'let a = 1, b = 2;')
test('(let (a))', 'let a;')
test('(let (a) (b))', 'let a, b;')
test('(const (a 1))', 'const a = 1;')
test('(const (a 1) (b 2))', 'const a = 1, b = 2;')
test('(const (a))', 'const a;')
test('(const (a) (b))', 'const a, b;')

//object
test('(object (a b))', '({ a: b });')
test('(object (a b) (c d))', '({a: b,c: d});')
test('(object)', '({});')
test('(object (get a () 1))', '({ get a() {1;} });')
test('(object (set a () 1))', '({ set a() {1;} });')
test('(object (get a () 1) (set a () 1))', '({get a() {1;},set a() {1;}});')
test('(object (get a () 1) (b 2) (set a () 1))', '({get a() {1;},b: 2,set a() {1;}});')
test('(object (get a))', '({ get: a });')
test('(object (set a))', '({ set: a });')
test('(object (* a () 1))', '({ *a() {1;} });')
test('(object (a))', '({ a });')
test('(object ((_[] a) 1))', '({ [a]: 1 });')

//class
test('(class a (extends b) (c (d) e))', 'class a extends b {c(d) {e;}}')
test('(class a (extends b) (c (d) e) (f (g) h))', 'class a extends b {c(d) {e;}f(g) {h;}}')
test('(class a (extends b) (static c (d) e))', 'class a extends b {static c(d) {e;}}')
test('(class a (extends b) (get c (d) e))', 'class a extends b {get c(d) {e;}}')
test('(class a (extends b) (set c (d) e))', 'class a extends b {set c(d) {e;}}')
test('(class a (extends b) (* c (d) e))', 'class a extends b {*c(d) {e;}}')
test('(class a (extends b) (static * c (d) e))', 'class a extends b {static *c(d) {e;}}')
test('(class a (extends b) (static get c (d) e))', 'class a extends b {static get c(d) {e;}}')
test('(class a (c (d) e))', 'class a {c(d) {e;}}')
test('(class a (c ()))', 'class a {c() {}}')
test('(class a (static (d) e))', 'class a {static(d) {e;}}')
test('(class a (get (d) e))', 'class a {get(d) {e;}}')
test('(class a (set (d) e))', 'class a {set(d) {e;}}')
test('(class a (static get (d) e))', 'class a {static get(d) {e;}}')
test('(class a (static set (d) e))', 'class a {static set(d) {e;}}')
test('(class a (* get (d) e))', 'class a {*get(d) {e;}}')
test('(class a (* set (d) e))', 'class a {*set(d) {e;}}')
test('(class a (static * get (d) e))', 'class a {static *get(d) {e;}}')
test('(class a (static * set (d) e))', 'class a {static *set(d) {e;}}')
test('(class a ((_[] b) ())', 'class a {[b]() {}}')

I'm still toying w/ it and will probably replace the _[] atom w/ something else, but the idea is to eventually sugar the computed key form w/ a reader macro e.g. (class a ([b] ())) => class a {[b] () {}}

lhorie avatar Oct 09 '15 01:10 lhorie

Not too sold on that class syntax, though. I'm too busy fixing my computer to type out a detailed reply, but I don't like having to use the extends keyword. It just doesn't feel right to me.

On Thu, Oct 8, 2015, 21:34 Leo Horie [email protected] wrote:

Looks very similar to what I currently have in that toy compiler I've been working on, except that I use a special form for computed values instead of the other way around.

For comparison, here's what some of my tests look like right now:

//variable declarationstest('(var (a 1))', 'var a = 1;')test('(var (a 1) (b 2))', 'var a = 1, b = 2;')test('(var (a))', 'var a;')test('(var (a) (b))', 'var a, b;')test('(let (a 1))', 'let a = 1;')test('(let (a 1) (b 2))', 'let a = 1, b = 2;')test('(let (a))', 'let a;')test('(let (a) (b))', 'let a, b;')test('(const (a 1))', 'const a = 1;')test('(const (a 1) (b 2))', 'const a = 1, b = 2;')test('(const (a))', 'const a;')test('(const (a) (b))', 'const a, b;') //objecttest('(object (a b))', '({ a: b });')test('(object (a b) (c d))', '({a: b,c: d});')test('(object)', '({});')test('(object (get a () 1))', '({ get a() {1;} });')test('(object (set a () 1))', '({ set a() {1;} });')test('(object (get a () 1) (set a () 1))', '({get a() {1;},set a() {1;}});')test('(object (get a () 1) (b 2) (set a () 1))', '({get a() {1;},b: 2,set a() {1;}});')test('(object (get a))', '({ get: a });')test('(object (set a))', '({ set: a });')test('(object (* a () 1))', '({ a() {1;} });')test('(object (a))', '({ a });')test('(object (([] a) 1))', '({ [a]: 1 });') //classtest('(class a (extends b) (c (d) e))', 'class a extends b {c(d) {e;}}')test('(class a (extends b) (c (d) e) (f (g) h))', 'class a extends b {c(d) {e;}f(g) {h;}}')test('(class a (extends b) (static c (d) e))', 'class a extends b {static c(d) {e;}}')test('(class a (extends b) (get c (d) e))', 'class a extends b {get c(d) {e;}}')test('(class a (extends b) (set c (d) e))', 'class a extends b {set c(d) {e;}}')test('(class a (extends b) (_ c (d) e))', 'class a extends b {c(d) {e;}}')test('(class a (extends b) (static * c (d) e))', 'class a extends b {static *c(d) {e;}}')test('(class a (extends b) (static get c (d) e))', 'class a extends b {static get c(d) {e;}}')test('(class a (c (d) e))', 'class a {c(d) {e;}}')test('(class a (c ()))', 'class a {c() {}}')test('(class a (static (d) e))', 'class a {static(d) {e;}}')test('(class a (get (d) e))', 'class a {get(d) {e;}}')test('(class a (set (d) e))', 'class a {set(d) {e;}}')test('(class a (static get (d) e))', 'class a {static get(d) {e;}}')test('(class a (static set (d) e))', 'class a {static set(d) {e;}}')test('(class a ( get (d) e))', 'class a {get(d) {e;}}')test('(class a ( set (d) e))', 'class a {*set(d) {e;}}')test('(class a (static * get (d) e))', 'class a {static *get(d) {e;}}')test('(class a (static * set (d) e))', 'class a {static *set(d) {e;}}')test('(class a ((_[] b) ())', 'class a {b {}}')

I'm still toying w/ it and will probably replace the _[] atom w/ something else, but the idea is to eventually sugar the computed key form w/ a reader macro e.g. (class a (b)) => class a {b {}}

— Reply to this email directly or view it on GitHub https://github.com/anko/eslisp/issues/23#issuecomment-146732974.

dead-claudia avatar Oct 09 '15 04:10 dead-claudia

@impinball both class id and superClass are optional in class expressions, so (= x (class a)) would be ambiguous if both id and superClass were simply identifiers. (i.e., is it x = class a {} or x = class extends a?)

I'm currently doing it this way to maintain consistency with the keyword verbosity of other constructs, i.e. I have (catch ...), (finally ...), (else ...), (case ...), (default ...) forms because those are js keywords, even though the estree spec does not specify node types for all of those keywords.

I'm aware that some of my choices are different from current eslisp. For example, I have (if a b (else c d)) instead of (if (block a b) (block c d)) to make if more consistent w/ other statement types like try.

Regardless, eslisp forms don't need to be the same as my toy compiler, and my code is still heavily in flux and I'm more than happy to hear feedback and suggestions.

lhorie avatar Oct 09 '15 12:10 lhorie

Yeah... They're different languages. And my opinion isn't exactly required to implement. Plus, we kinda need to sort out objects before moving on to classes here, since class methods should have similar syntax to object methods.

On Fri, Oct 9, 2015, 08:58 Leo Horie [email protected] wrote:

@impinball https://github.com/impinball both class id and superClass are optional in class expressions, so (= x (class a)) would be ambiguous if both id and superClass were simply identifiers. (i.e., is it x = class a {} or x = class extends a?)

I'm currently doing it this way to maintain consistency with the keyword verbosity of other constructs, i.e. I have (catch ...), (finally ...), (else ...), (case ...), (default ...) forms because those are js keywords, even though the estree spec does not specify node types for all of those keywords.

I'm aware that some of my choices are different from current eslisp. For example, I have (if a b (else c d)) instead of (if (block a b) (block c d)) to make if more consistent w/ other statement types like try.

Regardless, eslisp forms don't need to be the same as my toy compiler, and my code is still heavily in flux and I'm open to suggestions.

— Reply to this email directly or view it on GitHub https://github.com/anko/eslisp/issues/23#issuecomment-146863379.

dead-claudia avatar Oct 09 '15 18:10 dead-claudia

@impinball Many thanks for the summary. That'll make a useful base for tests.

anko avatar Oct 09 '15 20:10 anko

Isn't this inconsistent with the . macro? A quoted atom should evaluate to itself: in (. console log), log evaluates to log in JS. When defining the object, I'd expect the same behavior: (object (log (lambda …)))—because, again, log evalues to log in JS: {log: function() {}}.

stasm avatar Oct 09 '15 21:10 stasm

No, this is consistent with the planned changes to the (.) macro.

In your example, neither atom is quoted, so they shouldn't evaluate to themselves, but rather to the variable they name.

tabatkins avatar Oct 09 '15 21:10 tabatkins

Ah, of course, my bad! In my example, log doesn't end up as a reference to a variable.

stasm avatar Oct 09 '15 21:10 stasm

But it will, per #13. The current design is broken, as it's not compatible with computed property names.

tabatkins avatar Oct 09 '15 21:10 tabatkins

Yes, I see, it makes sense now :)

stasm avatar Oct 09 '15 21:10 stasm

No, this is consistent with the planned changes to the (.) macro.

:point_up: Correct.

Sorry, the confusion is my fault. #13 should have been marked open. We had some confusion about the exact nature of the problem.

anko avatar Oct 09 '15 23:10 anko

Made another edit to the main suggestion, per my comment in #13.

dead-claudia avatar Oct 13 '15 13:10 dead-claudia