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

Collect developer feedback about the ergonomics of #{ }/#[ ] (vs alternatives @[ ]/@{ } or {| }/[| ]) #10

Closed
littledan opened this issue Apr 4, 2019 · 480 comments

Comments

@littledan
Copy link
Member

littledan commented Apr 4, 2019

Notice by @rricard: This issue is now only open to discuss the following alternatives: #{ }/#[ ], @{ }/@[ ], {| |}/[| |] or {| }/[| ]


Original issue text:

Most people I've talked to are pretty positive about @const in two ways:

  • Since it applies deeply on the data structure, it's not too wordy to have these extra letters at the beginning. And a terser syntax (like #{ } / #[ ]) might be too cryptic and confusing.
  • The name @const makes sense, since it's just like declaring a variable const, but it goes deeply through the data structure.

It'd be good to continue collecting feedback to understand if these intuitions are widely shared.

@rricard
Copy link
Member

rricard commented Apr 4, 2019

For now it does not seem to bug people I showed that to but we should try to get more feedback surely

@robpalme
Copy link
Collaborator

robpalme commented Apr 5, 2019

How would @const interact with decorators?

@rricard
Copy link
Member

rricard commented Apr 5, 2019

That's a good question, I should address that in the proposal text

@silverwind
Copy link

I don't think it's a good idea to overload the const word, either via a const operator or @const. Why not use a new word like immutable <varname> = <expression> that acts just like var, let and const?

@arxpoetica
Copy link

I agree about not overloading const. Need a new word.

@rricard rricard changed the title Collect developer feedback about the ergonomics of @const Collect developer feedback about the ergonomics of const Jun 6, 2019
@rricard
Copy link
Member

rricard commented Jun 6, 2019

Ok so here is what we've seen so far:

  • @const - Pro: does not clash with existing syntax - Con: confusion about decorators
  • const - Pro (to me): looks conceptually the same as c++ const pointer and const value type duality - Con: confusable with the existing const
  • immutable - Pro: it's new it does not clash with existing - Con: it adds a new keyword is not necessary (using const in declaration and expression is differenciable)
  • using the #{}/#[] syntax - Pro: make it clear we manipulate an expression here - Con: might be confusing, especially with private fields/methods

@rricard
Copy link
Member

rricard commented Jun 6, 2019

I honestly do have a personal preference with const but if there is a strong rejection of it, I'm fine to discuss the other options.

@zenflow
Copy link

zenflow commented Jun 6, 2019

I don't think it's a good idea to overload the const word, either via a const operator or @const

I agree with this. This is one of the first issues I found with the proposal. It shouldn't be hard to pick a new keyword that doesn't imply equality with the existing semantics of const (an association which seems false to me).

I think immutable would be the perfect keyword (actually I would even change this in the name of this feature, since I didn't know what proposal-const-value-types could be until I read "immutable" in the description) but it's a bit long for a keyword.. not sure if that matters. immut? sealed? frozen?

I would even personally prefer #{} syntax to using the const keyword, but as @littledan said above, it's a bit cryptic, and would be hard to google.

@zenflow
Copy link

zenflow commented Jun 6, 2019

Con: it adds a new keyword is not necessary (using const in declaration and expression is differenciable)

It may not be necessary for compilers to parse the code properly, but I think it would be helpful to make things clear to developers.

To draw a parallel: the let keyword was not "necessary".. we could have reused the if keyword, and statements like if foo = 3 would have been compiled correctly, but it's not optimal for developers for obvious reasons.

@vweevers
Copy link

vweevers commented Jun 6, 2019

A new keyword would help in conversation (casual or educational):

  • "Could you help me with a bug? I've got this constant value.."
  • "A deeply constant value or a constant variable"?

Versus "I've got this immutable value".

@rricard
Copy link
Member

rricard commented Jun 6, 2019

That makes sense, I'll talk with my co-authors and see what we should do!

@getify
Copy link

getify commented Jun 6, 2019

I am deeply, deeply troubled by the conflation of const here.

Speaking as a frequent teacher of these exact topics (const declarations vs immutable data structures), I can confidently say that most learners will not find something like const obj = const { a: 1, b: 2 }; intuitive, but will on the contrary be even more confused and unlikely to understand the differences.

The claims in the README that const x and const { .. } are obviously distinct / orthogonal do not track at all with my experience.

@rricard
Copy link
Member

rricard commented Jun 6, 2019

This is completely relevant and I think I can definitely put aside my affinity to what C++ would do (as a matter of fact const const in C++ is a pain to teach) so let's find a replacement!

@getify
Copy link

getify commented Jun 6, 2019

Just to be clear, I love the feature. I want JS to have immutable data structures. I just don't like calling them "Constant Objects" or using const to make them.

FWIW, "immutable" may or may not be the right word, either. Generally, immutable data structures (in userland) mean data structures that allow structured mutations (via sort of copy-on-mutate behavior). If these are intended to have something like that -- awesome! -- then "immutable" is a good word.

But if not, if they're more like a deeply frozen object, then I think "frozen" or "read-only" are the more appropriate term than "constant" or "immutable".

EDIT:

After some clarifications in various tweet threads, I actually think "fixed" is better than "frozen" / etc.

EDIT 2:

if we go in the direction of "immutable", I like imm as a short useful keyword.

@ljharb
Copy link
Member

ljharb commented Jun 7, 2019

Since Object.isFrozen exists along with the concept of "frozen", for whatever term we choose here I'd also expect an API predicate to determine whether the term applies - ie, Object.isFixed, etc.

@Jamesernator
Copy link

Jamesernator commented Jun 7, 2019

An alternative syntax (and naming) could just be to use two decorators or tagged object literals and use the fairly popular "tuple" and "record" names:

e.g. With decorators:

const vector = @Tuple [3, 4]
const point = @Record { x: 10, y: 20 }

e.g. With tagged literals:

const vector = Tuple#[1, 2]
const point = Record#{ x: 0, y: 10 }

@ryan-codingintrigue
Copy link

I agree that fixed seems like a good option. Neither const nor immutable feel right because individually they only really describe one aspect of the behavior.

I can see Object.isFixed being a tad odd given the existence of Number.isFixed but the namespace should provide enough distinction

@kleinfreund
Copy link

What about a final keyword as in const point = final { x: 0, y: 10 };? Is there a documented reason why this wouldn’t work?

@getify
Copy link

getify commented Jun 7, 2019

One possible issue with the keyword approach is using this feature with primitives, specifically a template literal, since that would be ambiguous with template tags, or with regexes:

var x = fixed "string";  // ok
var y = fixed `string`;  // oops
var z = fixed / a / gi;  // oops

There's no advantage of using these with primitives, per se, but I also think it might be strange (with a keyword rather than syntax) to explain that it can only be used with array/object values and not other primitives.

The mental model I'm using here is that a value-type is basically a bit like defining some arbitrary array/object as a single primitive value unit. So it'd be a shame if this feature couldn't reflect that symmetry.

@kleinfreund
Copy link

One possible issue with the keyword approach is using this feature with primitives, specifically a template literal, since that would be ambiguous with template tags, or with regexes:

Would that only be an issue for a keyword (e.g. `fixed) if it’s already in use in a lot of code (e.g. in a tagged template literal)?

There's no advantage of using these with primitives, per se, but I also think it might be strange (with a keyword rather than syntax) to explain that it can only be used with array/object values and not other primitives.

I agree.


It seems like there are two fundamental questions worth investigating: What to call it (e.g. const, fixed, final, immutable, etc.) and what kind of language structure should be used (e.g. keyword, at-prefixed name, the weird bracket syntax).

@TehShrike
Copy link
Contributor

TehShrike commented Jun 7, 2019

A note on final – that keyword is used in Java to indicate variables that will not be reassigned (the equivalent of const variables in JS right now).

@arxpoetica
Copy link

@getify wait, what's wrong with this?

var y = fixed `string`;  // oops

@ljharb
Copy link
Member

ljharb commented Jun 7, 2019

@arxpoetica that already works by using the identifier “fixed” as a template tag.

@arxpoetica
Copy link

oh oh yeah. hmm. so maybe

var y = @fixed `string`

?

@hormesiel
Copy link

hormesiel commented Jun 7, 2019

Disclaimer: I probably don't know what I'm talking about but I'll give it a try anyway.

Among the following syntaxes :

// const
const obj = const { a: 1, b: 2 };

// fixed
const obj = fixed { a: 1, b: 2 };
fixed obj = { a: 1, b: 2 };

// final
const obj = final { a: 1, b: 2 };
final obj = { a: 1, b: 2 };

// frozen
const obj = frozen { a: 1, b: 2 };
frozen obj = { a: 1, b: 2 };

// immutable
const obj = immutable { a: 1, b: 2 };
immutable obj = { a: 1, b: 2 };

immutable seems like the most intuitive and straightforward solution to me. It's so obvious what it does you don't even need to explain it! Which is not the case for most of (if not all) the other proposed keywords / syntaxes.

Also, I prefer the RHS solution because it means that you can do :

let obj = immutable { a: 1, b: 2 };
obj = null; // OK
obj = { ... }; // OK
obj = immutable { ... }; // OK

function fn() {
  // ...
  return immutable { ... }; // OK
}

obj = fn(); // OK

and reuse the obj variable, while the LHS version could lead to unexpected results in some cases and would have annoying limitations :

immutable obj = { a: 1, b: 2 };
obj = null; // ???
obj = { ... }; // ???
obj = immutable { ... }; // Not possible just like you can't do `obj = const { ... };`

function fn() {
  // ...
  return immutable { ... }; // Not possible just like you can't do `return const { ... };`
}

Now if a 9-letter keyword is too long I think immut could be a great fit too.

@shannon
Copy link

shannon commented Jun 7, 2019

Please forgive me if I am not fully understanding this proposal but what's the advantage of explicitly assigning the structure to a variable?

If a keyword is used could you not just go the way of function?

fixed map1 {
    a: 1,
    b: 2,
    c: 3,
};

const map2 = map1 with .b = 5;

assert(map1 !== map2);
assert(map2 === fixed { a: 1, b: 5, c: 3}); //<- something like an anonyomous fixed

If this were the case I would think something like struct would make sense as a keyword but I'll admit I don't know the full implications of this when compared to other languages.

@shannon
Copy link

shannon commented Jun 7, 2019

I also don't know what the possibility if it being "called" like a function as well instead of using with

const map2 = map1{ b: 5 };
//or with an array
const array2 = array1{ 0: 'x' }

As far as ergonomics go that's pretty easy. I know this doesn't say much about the understand-ability, teach-ability, or possible confusion when reading at a quick glance.

@rricard
Copy link
Member

rricard commented Jun 8, 2019

Alright everyone, we do appreciate the feedback but let's keep that issue on point: the keyword.

I don't think the const keyword will be accepted, however adding a new keyword can have bad effects, notably parsing existing javascript. Reusing an existing keyword in a different situation is a good way out of that issue (you can't name a variable const, same with with).

From there if we do not go with const we'll have two other solutions:

  • go back to the original idea of a decorator (@const originally but could be @immutable or @fixed or @final
  • use the # symbol as it would not clash with private field syntax

I'm going to rephrase the proposal so it is open ended between const/#/@const/@immutable/@fixed/@final.

@acutmore
Copy link
Collaborator

I am wondering if a shorthand and long form, would be possible?
...
I would probably prefer Tuple{} or Record{} but can imagine that is not allowed syntax.

From a certain perspective there is already a long-form available if an author feels that would help with readability:
Record({ a:1, b:2 }) and Tuple(1, 2, 3)

Though these are technically slightly less efficient with the extra allocation and function calls, though that may only be an issue in performance sensitive code paths.

@getify
Copy link

getify commented Feb 22, 2022

if an author feels that would help with readability

You can create objects with new Object(), and then add properties one-by-one. In that line of reasoning, this is "more readable" in that it's the "longer form" and more descriptive of what you're creating. And you might be able to artificially construct a "more readable" claim in some niche corner case.

But I think it's pretty strong consensus that { .. } literal syntax is pretty universally preferred over the new Object() + individual-property-assignments form. Yes, there's a performance justification (the engine can optimize with the literal form since it can statically analyze the shape of the object and pre-allocate), but I actually think the performance aspect is far secondary to the belief that the literal form actually communicates more clearly than the constructor-form (rather than the opposite).

I'm not claiming such because it's shorter, but because it's so much more recognizable due to its declarative nature. I think JS would be worse-off if the reverse were true, and everyone preferred and used the constructor-form over the literal form.

I would expect, and hope, that whatever syntax we pick for records/tuples will, eventually, achieve the same "universal best practice" preference over whatever long-form API constructor form (if any) has also been added. That should be our goal, anyway (IMO).

@dy
Copy link

dy commented Feb 22, 2022

What makes Record/Tuple literal more necessary than Map/Set?

@getify
Copy link

getify commented Feb 22, 2022

What makes Record/Tuple literal more necessary than Map/Set?

I do kinda wish Map/Set had a literal syntax form. If such a form existed, I'd prefer it over the constructor forms.

@svieira
Copy link

svieira commented Mar 4, 2022

I do kinda wish Map/Set had a literal syntax form.

They could if we could add SomeKindOfConstructor#{ } and AnotherKindOfConstructor#[] or some such construct to the language (Space would be a great character, except Identifier [] already means something in JS). Then Map#{ [key]: value } (or even Map#{ key: value } if we wanted the left-hand side to be a variable reference by default) would be possible. And that would also let us have ImmutableSortedMultiMap#{ [x]: 1, [x]: 2, [y]: 33 } (and get (ImmutableSortedMultiMap { valueOfX: [1, 2], valueOfY: [33] }) without having to add it to the language.

@opensas
Copy link

opensas commented Mar 4, 2022

I don't think it's a good idea to overload the const word, either via a const operator or @const

Just like many people in this thread (@silverwind @arxpoetica @zenflow ...) I completely agree with this.

I mean, having immutable structures baked in, is one of the best things that could happen to JavaScript.

But I think it would be much clearer to have a new keyword instead of the # prefix, and overloading const will only add to the confusion (even more than what the present const already brings to the table)

So like many in this thread I would propose a new keyword, in this case val.

Reasons:

  • developer ergonomics: just three letters
  • consistent with the rest of the keywords, var, let, const and now val.
  • it's pretty clear the opposition between var that stands for variable and val that stands for value
  • similar to regular primitive values these val are:
    • immutable
    • passed by value
    • compared by value
      will be compared by their contents
  • it is already used in other languages, like scala and kotlin, with a similar intention
  • highly subjective, but IMHO it feels better than imm

@noppa
Copy link

noppa commented Mar 4, 2022

The reason const is in the table is that it, unlike val, is already a keyword. Overloading existing keywords may feel icky but introducing new ones is just untenable. val [0] is valid syntax today and can't be repurposed. @val maybe, but then it's again at least confusing with decorators.

@matthew-dean
Copy link

matthew-dean commented Mar 5, 2022

@svieira Your example is interesting because you could almost make that a language feature where:
Foo# value === new Foo(value)

Therefore, presuming you would you would create a new record with new Record({ plainObject }) you could do:

const R = Record
const myRecord = R#{ plainObject }

Then people could choose the ergonomics they wanted, to some degree. But of course that's not quite as sweet / simple as #{ plainObject }

To do the Map example, they could do:

let map = Map#[
  ["1", "first element"],
  [1, "second element"],
  [true, "hello world"]
];

Buuuuuut I wonder if all of that becomes hella complicated when you want to represent / serialize the thing you've just created. Or if that's just too simple of syntactic sugar to be useful.

That said, I just logged the above Map in Chrome and it logs as:

Map(3) {'1' => 'first element', 1 => 'second element', true => 'hello world'}

Which then leads to this rational (and very good) question from @dy:

What makes Record/Tuple literal more necessary than Map/Set?

Like... if there's no "easy" syntactic way to define Map or Set, why are Records and Tuples special in that regard? They're all collections. I guess the question has been sort of reversed as, "We have simple literals for arrays and objects, so why wouldn't records and tuples?" But you could almost answer that with: "Because JavaScript history, but as we added more types of collections, we didn't add new literals each time."

To me, it almost reads better to have something serialized like:

Record { value: true, nested: { foo: 'bar' } }

And not:

#{ value: true, nested: #{ foo: 'bar' } }

So it kinda begs the question of the value of defining it using a special syntax, especially, yeah, with the existence of Map and Set. It really depends on from which direction you're approaching an idea. Is this an "extension" of arrays and objects? Or are these new collections, just like Map and Set (and others) were? In my mind, records and tuples have enough (necessary) unique behavior that I wonder if this effort is sort of conflating them too much with the other JS structures they sort-of resemble. 🤔 🤔 🤔

@getify
Copy link

getify commented Mar 5, 2022

I think the biggest reason we want syntax for records/tuples is the presumption that it will be quite common to pre-define values with N levels deep of nesting. A "keyword" approach gets much uglier when you have to repeat it at every level of nesting, even if that keyword is a single character.

As to why records/tuples but not maps/sets literal syntax, I'm just sorta estimating here based on anecdotal observation, but it seems much more rare to me that you pre-define maps/sets (they're more often used for programmatically filled in data) and even less often have I seen maps/sets heavily nested. It's pretty common to see objects/arrays as literals with plenty of nesting. Perhaps we'd see more pre-defined maps/sets (with nesting) if there was a literal syntax for it... not sure, I think it's hard to ascertain cause/effect/correlation.

Another thing to point out about map/set constructor forms... both constructors have a pretty useful capability to receive, as input, arrays of values or key/value tuples to construct the map/set from. We don't really have such capability without the constructor, so a literal form seems somewhat less powerful or useful.

@shicks
Copy link

shicks commented Mar 5, 2022 via email

@matthew-dean
Copy link

matthew-dean commented Mar 8, 2022

@shicks

IANA VM Implementor, but I would assume that there are also performance
considerations here. With special syntax, the VM can skip instantiating a
whole extra object/array and allocate the record/tuple directly, whereas if
it's just syntactic sugar over a general constructor call, that's much less
feasible

This is an excellent point! I remember this occurring to me a long time ago, that these JS collections would probably map to optimized C structs (or something like that), but then I forgot about it. Thank you!!

@getify Also excellent points. It was just kind of a derp moment, like, "Yeah... WAIT a minute..."

@khaosdoctor khaosdoctor mentioned this issue Mar 15, 2022
@khaosdoctor
Copy link

Adding my 2 cents as mentioned before, and also on #82 and #9 I'm really leaning towards @getify in calling it using the immutable or imm keywords, I guess having a literal way of putting it like #{} or #[] is not bad as well, but it seems a bit cryptic if you skim through the code. So having something like:

imm name = { obj: 1 }
immutable other = { other: 'obj' }
const literal = #{ yet: 'another' }

Is something that makes it a lot clearer for me.

@getify
Copy link

getify commented Mar 15, 2022

@khaosdoctor I think it's important for the indicator of record/tuple to be attached to the value expression rather than to the declaration. There are lots of places values exist in programs that aren't part of being assigned first to a lexical identifier. For example, passing a value as an argument to a function call, or assigning the value to a property in an object.

@xianghongai
Copy link

放开私有属性和方法关键字符号化后,JavaScript 在脚本语言的路上越走越远了。但是 JavaScript 的应用场景越来越需要面向对象了,而不是胶水语言。我们在设计语言时,应关注本身形态和灵魂,而不是符号化去逃避历史负担。

After letting go of private property and method keyword tokenization, JavaScript went further and further as a scripting language. But JavaScript application scenarios increasingly require object-oriented, rather than glue language. we should focus on its own form and soul, rather than symbolize it to escape the burden of history.

@ljharb
Copy link
Member

ljharb commented Mar 20, 2022

@xianghongai im not sure what you’re getting at exactly. Nothing about JS is “glue language”, including private fields, records, or tuples, and most JavaScript applications i see these days actively avoid object-oriented code in favor of functional code.

@khaosdoctor
Copy link

khaosdoctor commented Mar 21, 2022

@getify 's

@khaosdoctor I think it's important for the indicator of record/tuple to be attached to the value expression rather than to the declaration. There are lots of places values exist in programs that aren't part of being assigned first to a lexical identifier. For example, passing a value as an argument to a function call, or assigning the value to a property in an object.

Makes sense, so you mean that:

const name = imm { obj: 1 }
const other = immutable { other: 'obj' }
const literal = #{ yet: 'another' }

function foo (bar) {}

foo (immutable { a: 1 })
foo (imm { a: 1 })
foo (#{ a: 1 })

Would be better? Makes a lot of sense to me, it'd be prettier with highlighting but I'm not against it.

@matthew-dean
Copy link

@khaosdoctor Your examples only sort of work when the immutable value is the "outer" value. How would it look when it is deeply nested within another object or array?

const immutable = 'immutable';
const myArray = ['immutable', immutable, { other: 'obj' }, immutable { other: 'obj' }, immutable { other: immutable }, { other: immutable {} }]

I feel like it starts to feel messy really quickly.

@glen-84
Copy link

glen-84 commented Apr 24, 2022

FMI, in which comment was the [| |] syntax rejected?

@rricard
Copy link
Member

rricard commented Jul 7, 2022

After using consistently the #{ }/#[ ] syntax in documents, examples, playground, conference talks, etc... we found no technical issue with the syntax that would discard it from being used.

The main pushback against it seems to be based personal preferences, which also happens when trying out other alternatives.

Unlike what we discussed earlier here, we haven't managed to do neutral polling on this, but regardless, the change would be harder to enact after the time we spent discussing the current version if such polling were to be made.

We still collected data points by interviewing people and syntax raises two kind of responses:

  • either it is fine or even good, some people like the symmetry with private fields, # means guarantees to them (privacy and now immutability)
  • either there would be a preference to change it but it isn't a dealbreaker overall: more time in those interviews was usually spent discussing how to reference objects from R&T for instance

Going forward, we are going to keep #{ }/#[ ] and intend to go to Stage 3 with this.

@rricard rricard closed this as completed Jul 7, 2022
@glen-84
Copy link

glen-84 commented Jul 7, 2022

class Example {
  #field = #{ id: 1234 }; 😕
}

Hash symbols are haunting me. JavaScript is starting to look like Perl.

Perhaps Anders Hejlsberg could assist with the language design? (I don't mean that in an insulting way, I just don't think that things are moving in the right direction.)

@ljharb
Copy link
Member

ljharb commented Jul 7, 2022

Aesthetics are incredibly subjective, as is PTSD from specific other programming languages.

@mirus-ua
Copy link

mirus-ua commented Jul 8, 2022

Going forward, we are going to keep #{ }/#[ ] and intend to go to Stage 3 with this.

As for me, this is a good choice. We can't pick a unique char for each case as someone wants, but # is fresh and will be strongly associated with immutable data

@weyert
Copy link

weyert commented Jul 8, 2022

Yeah wrote a plug-in that changes my preferred syntax to this before compile time. Solved my problem with the proposed syntax :)

@opensas
Copy link

opensas commented Jul 8, 2022

Yeah wrote a plug-in that changes my preferred syntax to this before compile time. Solved my problem with the proposed syntax :)

great, that would be WeyertScript! ;-)

@mischkl
Copy link

mischkl commented Apr 9, 2023

As for me, this is a good choice. We can't pick a unique char for each case as someone wants, but # is fresh and will be strongly associated with immutable data

As mentioned in #10 (comment) # is currently strongly associated with private fields, not immutability.. Just in terms of how logical the language is for people learning it for the first time, I think @glen-84 makes a good point. We all understand why on a technical level # is almost the only available option, but to say it is fresh and obvious, when it's already in use for something completely different in the form of private fields, seems to me to be overstating a bit.

@luncheon
Copy link

luncheon commented May 8, 2023

An issue with # is that the precious symbol # will no longer be available for new operators and syntaxes in the future.

@matthew-dean
Copy link

@rricard

Going forward, we are going to keep #{ }/#[ ] and intend to go to Stage 3 with this.

Yay! Can't wait to see this implemented in engines and be able to use it for all kinds of applications.

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