Open anko opened 8 years ago
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.
@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:
{a: b}
{"a": b}
{[a]: b}
{a}
{get a() {}, set a() {}}
{get "a"() {}, set "a"() {}}
{a() {}}
{"a"() {}}
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.
{a: b}
{[a]: b}
{a}
{get a() {}, set a() {}}
[1] Well, I kinda fibbed a little with ClojureScript, mainly with regards to (defprotocol)
.
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
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}}
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 https://github.com/anko/eslisp/issues/23#issuecomment-145951386.
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.
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 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 ...)
@impinball Ambiguity: Does (var a b)
compile to var a = b;
or var a, b;
?
@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.
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 ()))
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))
Edit: added shorthand. Edit 2: edited member expressions per my suggestion in #13.
I have an idea for solving the object
dilemma:
'name
be a literal key, and name
be a computed key.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.
Omitting the value is the property shorthand.
(object ('prop))
(object ('prop prop))
Thanks, @tabatkins for catching this.
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.
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 },
}
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.
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?
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:
(object ('a))
=> {a}
.(object ('a b))
=> {a: b}
, or (object (a b))
=> {[a]: b}
.(object ('a (b) c))
=> {a(b) { c }}
get
, set
, or *
@tabatkins
Also, are you all okay with the implicit lambda?
Yeah, I got no problems with implicit lambda.
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] () {}}
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 https://github.com/anko/eslisp/issues/23#issuecomment-146732974.
@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.
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 https://github.com/anko/eslisp/issues/23#issuecomment-146863379.
@impinball Many thanks for the summary. That'll make a useful base for tests.
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() {}}
.
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.
Ah, of course, my bad! In my example, log
doesn't end up as a reference to a variable.
But it will, per #13. The current design is broken, as it's not compatible with computed property names.
Yes, I see, it makes sense now :)
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.
Made another edit to the main suggestion, per my comment in #13.
ES6 introduces dynamic property names in object expressions:
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.