Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The object macro is not ES6-friendly #23

Open
anko opened this issue Sep 28, 2015 · 30 comments
Open

The object macro is not ES6-friendly #23

anko opened this issue Sep 28, 2015 · 30 comments
Milestone

Comments

@anko
Copy link
Owner

anko commented Sep 28, 2015

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.

@tabatkins
Copy link

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.

@lhorie
Copy link

lhorie commented Oct 5, 2015

@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"() {}}

@dead-claudia
Copy link
Contributor

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).

@lhorie
Copy link

lhorie commented Oct 5, 2015

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

@tabatkins
Copy link

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}}

@dead-claudia
Copy link
Contributor

That looks way better.

On Tue, Oct 6, 2015, 14:14 Tab Atkins Jr. notifications@github.com 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
#23 (comment).

@lhorie
Copy link

lhorie commented Oct 7, 2015

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.

@tabatkins
Copy link

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?

@dead-claudia
Copy link
Contributor

@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 ...)

@anko
Copy link
Owner Author

anko commented Oct 7, 2015

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

@dead-claudia
Copy link
Contributor

@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.

@lhorie
Copy link

lhorie commented Oct 7, 2015

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 ()))

@dead-claudia
Copy link
Contributor

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
Copy link
Contributor

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?

@tabatkins
Copy link

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 *

@dead-claudia
Copy link
Contributor

@tabatkins

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

@dead-claudia
Copy link
Contributor

Also, are you all okay with the implicit lambda?

@tabatkins
Copy link

Yeah, I got no problems with implicit lambda.

@lhorie
Copy link

lhorie commented Oct 9, 2015

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] () {}}

@dead-claudia
Copy link
Contributor

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 notifications@github.com 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
#23 (comment).

@lhorie
Copy link

lhorie commented Oct 9, 2015

@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.

@dead-claudia
Copy link
Contributor

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 notifications@github.com 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
#23 (comment).

@anko
Copy link
Owner Author

anko commented Oct 9, 2015

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

@stasm
Copy link

stasm commented Oct 9, 2015

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() {}}.

@tabatkins
Copy link

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.

@stasm
Copy link

stasm commented Oct 9, 2015

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

@tabatkins
Copy link

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

@stasm
Copy link

stasm commented Oct 9, 2015

Yes, I see, it makes sense now :)

@anko
Copy link
Owner Author

anko commented Oct 9, 2015

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

☝️ Correct.

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

@dead-claudia
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants