-
Notifications
You must be signed in to change notification settings - Fork 113
A new proposal, maybe late maybe not. #264
Comments
The new syntax note: class MyClass {
private x = 100;
foo() {
console.log(x); // 100
}
} for protected property: class MyClass {
protected x = 200;
foo() {
console.log(x); // 200
}
}
class MyClassEx extends MyClass {
bar() {
console.log(x); // 100
}
} for internal access: class MyClass {
internal private x = new Object;
compare(b) {
console.log(x === b[internal.x]); // true
}
static compare(a, b) {
console.log(a[internal.x] === b[internal.x]); // true
}
} |
What if I want a local of the same name? |
This has been proposed and rejected several times. Please see the FAQ. |
@bakkot The new proposal have not that problem, reason for rejecting "private" is not sufficient! |
@jhpratt
so, Need to be careful about class f() {
protected x;
}
class f2 extends f {
foo(x) {
// i unknow has 'protected x', or know it but can not change that!
}
} Easy! To define alias with class f3 extends f {
private x as xxx;
foo(x) {
console.log(x); // arguments x
console.log(xxx); // protected x
}
}
// more
class f4 extends f3 {
foo() {
console.log(x); // protected x from f, or f3
}
} |
Nothing has ever prevented someone from creating a local with a valid identifier as a name. Why should that change? |
Have a private scope in context of class definitions, methods is sub-level block in the class context. when parent has 'x', sub-level block choice either override and new-name. same of: function MyClass() {
let x = 100;
function MyMethod() {
let x = 100; // override up-level
let xx = 1000; // new name
}
} in MyClass private scope, all identifier is valid and known, MyMethod is free either override and new-name. |
Perhaps I should be clearer. If I can create a local binding (which I think you're saying you can), how would I then access the private member? That's an absolute must for any proposal. Please provide a more thorough example and explanation. |
Maybe, you need a newly name for exist private name: class MyClass {
private x = 100;
...
private x as internal_own_x;
foo(x) {
console.log(x);
console.log(internal_own_x);
} }
// OR
private get internal_own_x() { return x };
foo(x) {
console.log(x);
console.log(internal_own_x);
} |
@jhpratt give a good idea to me? if dup identifier in scope or context, we can do what? |
I gave you a -1 because I don't believe you have fully thought this through, and that your proposal is not feasible. Your solution to my concern is hacked together. As I said in my previous comment, please provide a more thorough example and explanation. This is a venue for serious discussion. If you're not willing to take my concern seriously, and are looking to complain about receiving a "thumbs down", I suggest you reconsider what you're here for. |
It's price for choice of identifier or class's fields. for identifier, impact is absolute exist in context of the class's definitions, but concept simple, and easy, and short code, these are good side. |
no, I am discussing this issue seriously and formally. i accept any questions and things. so, try testcases? I implement parser and interpreter base prepack and babel-parser. For syntax, I tried combined all case of private/protected/public. but, dup definitions is bound in one context. this is choice, not design. |
@aimingoo how can i access the private data of another object with your proposal? One common example is |
Yes, it does. That FAQ entry addresses all proposals which use |
Thanks. a big problem, key issue. good! 👍 Have some design principles of visibility of the solution:
So we can only read them using protected or public method. ex: class MyClass {
private x;
get x() {
return x;
}
set x(v) {
x = v;
}
}
Or using simple `public as` syntax to automatically add those access methods:
```javascript
class MyClass {
private x;
public as x; // same above Based on these, have two ways for your question. Case 1 // class design based
class InternalMyClass {
protected hashCode;
static compare(a, b) {
let getter = MyClassHelper.prototype.getHashCode;
return getter.call(a) === getter.call(b);
}
}
// access protected scope in child-class
class MyClassHelper extends InternalMyClass {
getHashCode() { // this.getHashCode()
return hashCode; // return this.#hashCode
}
}
// publish the class only
export class MyClass extends InternalMyClass {
private as hashCode; // update or not
}
Have not public-interface to access private data of a/b, unless you design method to access them in class. so, need make a helper class to provide a interface for static method in above example. And, MyClassHelper and MyClass are inherited from InternalMyClass, can access protected scope with same one inherited private-key. Case 2 // or hijack skill
const ACCESS_HASHCODE = Symbol(); // any name or symbol
let internal_getter;
class MyClass {
private hashCode = 100;
[ACCESS_HASHCODE]() { // public at MyClass.prototype
return hashCode;
}
static compare(a, b) {
return internal_getter.call(a) === internal_getter.call(b);
}
}
internal_getter = MyClass.prototype[ACCESS_HASHCODE];
delete MyClass.prototype[ACCESS_HASHCODE];
Okay, pls try test cases at here: and here: You can checkout these branch and run test. @here |
You didn't really answer his question. How could you do |
@aimingoo You're not going to get anywhere with this as along as:
The first one is absolute. There's no getting around it. For the second, there's a work around. It's perfectly fine for the lexical class definition to disallow duplicate property names. However, that needs to be a purely lexical limitation. Beyond the declaration, any member or caller needs to be able to add public properties to the class without restriction. Of course, as soon as you do that, you introduce the need to differentiate between private and public property accesss. Hence the various approaches that have been tossed around ( Dealing with that is what puts proposals against the artificial " |
Thanks, We are discussing a key issue, so please allow me to talk a little more.
I have discussed these before, such as #148 . but We need a solution to these problems, specific. Now, my proposal The For implement, And base these, class MyClass {
compare(b) {
return hashCode === b.#hashCode;
}
static compare(a, b) {
return a.#hashCode === b.#hashCode;
}
} But, x_internal_getter.call(b)
-> private(b).x
-> (private b).x
-> #b.x // enter private of b first, next got x
-> b#x // enter private of b, and access x For syntax I hope this proposal has the ability to end discussions on concepts and implementation levels, and recommend private member syntax like
|
Fields aren’t a conceptual conflict - it’s a new concept. Properties that aren’t public is also not a concept that exists. Inventing one or the other is the same thing - adding a new concept to the language. “ugly” is subjective, and not everyone agrees with this, so it’s probably best to remove this consideration entirely unless comparing two semantically identical forms (which isn’t the case here). Any proposal that does not allow for the static compare method i mentioned earlier is a nonstarter for me, and i suspect for other committee members too. |
Indeed, fields are a new concept, but that concept is in conflict with the existing concepts of ES that carry a similar purpose. Ignoring private fields for a moment, public fields are instance properties provided by the What's worse is that this very conflict becomes even more evident in the "trade-offs" that had to be made to arrive at the class-fields proposal, and the further trade-offs that will have to be made by both developers who wish to use fields, and developers who use libraries containing classes with fields. So yes, there are indeed conceptual conflicts between the "new concept" of fields and the existing concepts in ES.
...but that's not the reason we shouldn't bother with this argument. In support of the "ugly" claim, I've yet to find a JS developer who didn't have an immediate negative reaction to the aesthetic of this syntax. While there are those who won't have that reaction on first blush, I'd wager they're in the minority. The reason we shouldn't bother with that argument is that, within the limitations of the approach taken,
..and even those of us who do not wish to see class-fields reach stage 4. Concurrent access to the private data of multiple instances is an absolute necessity. |
As we've discussed many times, this is objectively false; "own properties" exist; and many paradigms exist that don't use |
Conceptual conflict is absolutely existing. The Object is defined as "object is collection of properties" in ECMAScript. So if Field is not a property, it must not belong to the concept set of "Object's member (collection elements of object)"; if Filed is property, then public field must be equal to property, there is a conceptual conflict. This is the root of all existing contradictions. NOTE: "an object is a collection of zero or more properties." ECMAScript Overview part. |
@aimingoo and exotic objects also exist, and objects can have internal slots - they’ve never just been a collection of properties. Class fields merely expands their definition; there’s no conflict. |
... and we don't disagree here. Never have. However, if we ignore the fact that in ES we can construct a class that is an instance of itself, a class and an instance are 2 different things. A class is a factory that produces instances via a "template", and initializes the instance via a "constructor". Since the inception of ES, the means of sharing pre-defined properties and behavior has been to create an object containing those properties and behavior, and use it as a prototype for all subsequent structures being created. That's a class, with the template being the prototype object. There is one other long-standing approach: object factories. This approach re-creates each property and method new so that, unlike with a class, it cannot be said that 2 different instances created from a factory are of the same type. Even V8 would internally treat such instances as being of different types. So let me throw another log on the fire and say that this proposal also conceptually conflicts with the general concept of a class. In most other languages with classes, the entire structure of the class instance is known before even the first ancestor's constructor is run. This allows for situations like subclasses overriding functions that get called by ancestors during the constructor. If this proposal were to preserve the prototypal nature of a class, then that would still be the case as the prototype is applied before the constructor code using Understand, I'm not trying to spark yet another debate. I'm merely trying to point out that TC39 definitely took a conceptual left turn somewhere and ended up with a conceptually conflicted, trade-off ridden proposal with gotchas that cannot be conclusively described as worth while. |
In fact, that was part of the motivation for using [[Define]] - so you didn’t need to know about superclasses to statically know the entire (modulo arbitrary modifications you make in the constructor, which always is both a possibility and something engines know how to handle) shape of instances (which engines know now, because fields appear statically and lexically in the class body). |
That is rather famously not true. |
Much like different browsers implement JS in slightly different ways, C++ has also been implemented in slightly different ways over the decades. This is something that used to work with Borland C++ and Microsoft C++ back in the 90's. While it's still true that the derived class would not yet be initialized, the v-table would be. So it's not that the call would go nowhere (segmentation fault), but rather that the call would be sent to a function that may be expecting an already initialized set of instance properties. That means that making such calls is generally bad practice, not that making such calls couldn't be done. It may be hair splitting, but it's an important distinction. BTW, pointing out 1 language where that seems to not be true doesn't invalidate my statement. I didn't say "every language with classes". Further, even if you had successfully invalidated this one point, that doesn't lend any more credibility to the contradictions and conflicts embedded in class-fields, or in any way shake the arguments I've made regarding the nature of ES. In fact, the simple fact that you chose to attack a side example instead of the main point may appear to add more credibility to the point I made. If you wish to discredit my argument regarding the difference between classes and object factories, and how fields blurs the distinction, thus causing conceptual conflicts, please attack that directly. Unless your argument is logically flawed, I won't counter. |
Lord. No. I just didn't want readers to be come away with a mistaken belief about a question of fact. |
Just as a note: The main reason for allowing such a thing is to handle cases where the base class knows how to contain and move information, but doesn't know the precise shape of it, while the derived classes know and need to be able to initialize that precise shape before the completion of the base class constructor. Admittedly, this is usually a sign that composition might be a better choice than inheritance. However, I'm of the mindset that a language developer shouldn't be in the habit of dictating what a language user should and shouldn't do (unless the particular practice is always catastrophic). |
It would have been nice to know that back when many of us were struggling to understand the motivation for using [[Define]]. I realize it must be difficult to remember all the important points that were discussed about a topic and share them here, I just thought I'd let you know that I never saw that mentioned anywhere, even when I asked specifically if there were any other practical (and not just academic) issues in favor of [[Define]]. (Of course, it's possible this was mentioned in an earlier discussion that I wasn't a part of.) |
Back to @aimingoo's proposal... In addition to the technical problems, there's also a developer experience problem—the same one mentioned in the FAQ (which we've also discussed many times). If you can declare P.S. A shorthand syntax might be fine (that's another debate), but the longhand syntax |
It was mentioned in many issues; it’s also in the notes. |
Ah, my mistake then for missing it—sorry. |
That's one view. Another is that if you can declare Both are resolved in the same way, an explanation of the feature. Both require the understanding that private field is not a property of the object that owns it. That's sufficient to explain why you would need a redirecting operator to access a private field when the declaration is Also, the claim that Point being, there's a learning curve for both cases. Which one is steeper will depend on the person's development experience. As such, the forced implication between |
Yes, as I said before, this is a big problem. So I took a moment to fix it. Based on these principles:
So hard define / explicit indication are necessary. And next, we can try this syntax: class MyClass {
internal private x = 100;
compare(b) {
return x === b[internal.x];
}
static compare(a, b) {
return a[internal.x] === b[internal.x];
} Or checkout these testcases: Okay. now, the
Thanks all. The new proposal at private property, try in real environment base on proposal-private-property branch @prepack-core. |
I feel that:
Now, despite all of that, I'm sorry @aimingoo, but your proposal, even after providing this adjustment, still will not satisfy TC39, or even me. The meaning of your use of Consider this: while @aimingoo My point is that if you want to suggest something better to them, it must at least fit the 1st order result equivalence of the conceptual |
In the past few days, it was the traditional Mid-Autumn Festival in China, so I did not reply to you in time. Thank you for your understanding.
For exotic objects an internal slots, they do not change the external interface of the objects, A object is always accessed as a collection of properties. But in the description of @rdking
Based on "prototype inheritance" really can't solve the existing problems? Or "prototype inheritance" is not good enough for everyone to give up this path? ES6's Don't try to make things simple by adding things, usually it gets more complicated. And And, In the definition of conceptual syntax, I did make mistakes. I didn't realize that there needed a more complete and accurate definition. Thank you for pointing it out. I will fix it. |
@aimingoo public class fields create properties, just like code in a constructor does; private class fields do not. There's nothing "different" that's exposed whatsoever - you can't even determine via reflection that a class has a field as compared to an Object.defineProperty call directly in the constructor. There is no contradiction here, and zero new concepts from the perspective of a consumer of the class - only from the perspective of the author of a class. |
The external interface of an object is already more than a set properties, like the call signature and constructor signature, they're part of the shape not defined by any property (it doesn't rely on interface Foo {
prop: string // property signature
(): boolean // call signature
new (): Bar // constructor signature
} The non-property signatures are de-facto behavior of JavaScript objects, even included in Web IDL. Also, even without counting non-property signatures, an object can always have non-property state: const foo = { value: 1 }
const bar = { value: 1 }
const isValid = new Set([foo]) Even // Removed some content due to misunderstanding |
@trotyl @ljharb The simple fact that a "field" is a delayed instance property definition that cannot be edited is already something far outside of anything else the language does. No other means of defining properties in the language waits before performing the described task. No other means of defining properties in the language completely hides the definition structure before use. No other means of defining properties in the language takes action on an object other than the primary products or parameters of the action. The problem isn't whether or not it mimics existing techniques. It does that marvelously. The problem is that it is essentially snake oil. What it presents itself to be (codewise), and what it actually is are two entirely different things, and it causes problems that are core to the conceptual nature of a class. That's going to present problems for ES users coming from compiled OO languages, and ES users with no TypeScript or Babel experience. The subset of the ES community that I just described should not be blithely dismissed. |
@rdking you’re also describing “code inside a function that creates a property” - you edit it the same way, by changing the source code. This is nothing new to consumers; and only a new way to write it for authors. The “other means” that already exists is called “a function”, and it does all these things if you wish. I’m not addressing the mental model or intuition claims you’re making in this thread - merely that what it is is not conceptually new for consumers, and not conflicting. |
@ljharb This is where the hair splitting comes in.
That function body is magically produced from the contents of the class definition's field descriptions. So in a very real sense, there's no function source to edit. Only a class definition. This is why I keep calling fields "pseudo-declarative" and "delayed instance properties". Such functions will be the only things in the entire language that generate an inaccessible, deferred action. ES doesn't actually have
Look, even though I'm more than ready to shout it from the mountain tops that this is one of the poorest designs I've seen come out of TC39, there's actually nothing wrong with this proposal that cannot be avoided by simply not using it (which is what I'll continue to do). I just feel it a bit disingenuous to try to pass off what had to be done to make fields work as "nothing new to consumers". It's definitely new, and even conceptually, it's unlike anything that's been done in ES before. The only thing that's similar to what consumers are used to seeing is the instance properties it produces. But even in that, how common is it for someone to use Even where "fields" is relatively mundane, it still does things in an atypical way. That's what's going to lead to intuition and mental model failures. That's what's going to lead to bugs. |
It’s new to class authors. It’s not new - or even really visible - to class consumers, Set vs Define notwithstanding (which is an issue unrelated to the concept of fields). |
The real issue is that the language painted itself into a corner when it introduced ES6 classes. Releasing ES6 classes was an amazing feat that involved balancing many competing wishes and concerns of TC39 members and the community. But the downside of the "maximally minimal" approach that allowed that proposal to get out the door was that apparently very few people fully thought through the ramifications for the addition of class properties/fields, namely the very limited design space and syntax choices. (To be fair, some of the limiting factors were due to other design choices baked into the language before ES6 classes were introduced, so it was a combination of things.) So we could argue until the end of time about the decisions made for this class fields proposal, but the fact is that every single possible solution would compromise on something. Maybe the critics of this proposal are right that some alternative proposal (or even some slight tweaks of this one) would have been a better solution given the constraints, but the only reason it's such a contentious proposal in the first place is because these constraints mean that there's no way to make everyone happy; in fact there's probably no way to avoid making some people very unhappy. So let's not pretend that the language would be perfectly consistent with itself or perfectly intuitive to everyone "if we just did such-and-such". ES6 classes already introduced a mismatch between the syntax (or at least many developers' expectations of that syntax) and JS's existing object model, and that was probably unavoidable given the goal of more declarative syntax for "classes". Whether you believe that the class fields proposal makes that mismatch better or worse, the most someone can claim is "my compromise is better than your compromise," not "my solution is unequivocally the best and the other is a disaster". And a compromise that ensures that at least public fields are consumed in exactly the same way as before (as class fields does) has a lot going for it. |
This is true. That's also why I'm so very interested in having TC39 come up with complete and thorough documentation of the concerns that factored into this proposal. Honestly, with any design project, a living document containing those concerns (Requirements Document) should always be created. Here, all we have is "the FAQ" which while indeed documents 7 or 8 of those concerns, doesn't come close to explaining why the approach ended up being what it is. Obviously, there were more concerns involved, more requirements than what is contained in "the FAQ". The point of having such a document is that it gives everyone concerned a chance to weigh out the competing concerns in a manner that is clear to everyone. My perspective has always been that technical concerns outweigh emotional concerns for software development. That's why although I don't like the sigil, I won't argue about it. That's why I firmly believe The flags I'm raising aren't about what I feel would be better, but rather about how expected (if not needed) functionality is being traded away. What we're getting in return has significantly less technical value to the class developer and class user than what we have to give up to get it. I will never believe that making something that was already simple more ergonomic is worth the cost of disrupting well understood development patterns. I will never believe that extending the coverage of an existing problem is a good idea when adding something new. And I am 100% certain that the visible semantics of this proposal can be achieved without violating these beliefs.
Given that ES6 classes were designed to be prototype-based like ES5-style classes, the attempt to pursue Java-like fields doesn't make sense. If they wanted to do that, then classes should have been designed with appropriate layering so that the data from ancestor classes is isolated to its own instance layer in the prototype chain. That was not done. So since the foundation isn't there, why does it make any sense at all to try to emulate Java fields? I'm not claiming any one approach to be "unequivocally" superior, just that the approach taken is self contradictory in the face of the existing language structure, and that those contradictions are the source of the numerous problems with this proposal. Knowing that less contradictory proposals can be constructed (and even have been) is enough for me to claim this proposals technical inferiority. Note that my statements are always categorical, never absolute. |
There is a basic conceptual question: For the so-called private property, is action of object to access a name in the private domain of its class, or action from its class and to access object (Own)private member? For the 1st understanding, since the private property is actually owned by the class, the object just sees and uses it. So equivalent to class-opening its private domain. in this case, the class needs to create a scope of the class, and manage the visibility of the name (properties) in the scope, which is responsibility of class - A class is the creator of an object. This concept is correct, achievable, and does so in many other languages. But this does not apply to JavaScript. Because in JavaScript, the class is just the holder of the object prototype (MyClass.prototype), the class is not responsible for the inheritance of the object, nor is it responsible for the visibility of the object members. The class does not maintain any scope for the object. Especially the latter, which means that if you want to implement private/protection/... on the basis of this concept, you need to build a complete set of scope management based on inheritance relationship between multiple dimensions (actually maintain the class, the scope, instances of class). This is why the more complicated the discussion of Then let's discuss the 2nd understanding. If the object itself holds a private property, then in principle the object itself can access it. The ability of an object to "access itself" is achieved through its own methods or prototype methods. So strictly speaking, accessing private properties in these two methods should be unlimited. For example (the following example attempts to illustrate that the "object method" should know that it is a private member of the action object when accessing obj = {
private x: 100,
foo() {
console.log(x); // own methods, unlimited
}
}
// OR
class MyClass {
private x = 100;
foo() {
console.log(x); // prototype methods, unlimited for instances
}
}
a = new MyClass; So, is the restricted when class accessing its object instances? In traditional languages, if this is the static lexical scope of the class, then no problem, you can let the owner of the class or class manage this as a scope. However, in JavaScript, there is actually no such thing as a "scope of a class". Instead, a class has its own private property (and a private domain) when it is treated as an object. There are still two choices here. One is that the class (as an object) continues to hold its own private domain, and the other is to move it into a "class lexical scope" that does not yet exist, and is managed by the class. The latter requires a new mechanism, which is costly, and as mentioned before, "an inheritance will rebuilding again." So according to the existing language features, the more realistic approach is to let the class (as an object) hold its own private property. However, in this way, the behavior of accessing the private properties of a concrete instance needs to be explicitly indicated, such as the so-called
It is precisely because the class and the object have the same concept of the private scope, and base the second interpretation of the above, "the private domain is the object's own (Own)", so when class to access "private scope of object" must be Explicit
And, indeed, even if we ignore the problem of class MyClass {
protected x = 100;
}
class MyClassEx extends MyClass {
static foo(a) {
console.log((private a).x);
}
} So, it’s better to face it: because the private members of the object are their own, the private member access of Now we have come to our description of the conceptual syntax
The conceptual syntax only indicates the existence of the private scope of
Finally, I have no intention of discussing any behavior that attempts to control the number of characters in source code by reducing or shortening the keyword. I explicitly prefer to be explicitly express (necessary) abstract concepts. The semantic clarity of the code text is very important, far more important than how much more or less it is in the number of bytes of code; the beautiful of the code formal or style is not achieved by controlling characters and symbols. But even so, for any implementation of What I want to emphasize is, the syntax style is not the core issue of the |
Correct. Compared to the other issues with this proposal, the syntax isn't even worth considering a problem.
Well, before I try to answer this, there were several major flaws in your analysis, not the least of which was:
That statement isn't correct. Classes have a "lexical scope" just like every other structure in the language. That lexical scope and what is done with it has very real consequences on what can be done with a class. Try factoring that into your analysis and watch how it changes. Getting back to the question, you asked an "a or b?" type question without realizing there's a "c". Here's the possibilities:
Class fields is built on concept 3. In fact, most of the good proposals that were reasonable substitutes for this proposal used concept 3. There was 1 case of concept 4 (private-symbols). The reason for most proposals preferring concept 3 is because in an open access system like ES, if you can't protect the property names, then those properties are essentially public. Case 1: Case 2:
This cannot be assumed to be true. Consider an object with private data and an accessing function like I could continue to argue the details here, but I think this should be enough for you to make some adjustments in your understanding. |
I think, I can simply tell the idea of my implementation, not to conceptualize it.
OVER. @rdking Maybe we can discuss the implementation directly. In addition, for the implementation of the
I must explain that in the above implementation of conceptual syntax. Base on existing design of es6 class, the private domain (of instances and their class) is two separate, unconnected domains , which is why [[Internals]] is shared. |
I don't mind, but it shouldn't be done here as it would no longer be about class-fields. |
@rdking Oh ha, welcome to new proposal. :) |
@rolivo The syntax issue is indeed minor. There are far larger technical and usability issues with this proposal that TC39 has decided to deem too small individually to merit stopping the proposal. However, the severity of some of those technical issues is greater than they have surmised. Further, the sheer number of technical issues compounded with breaks in usability and inconsistency with existing mental models makes it worth while to seek an alternate proposal IMO. I am one of those developers that wants to have real OOP support. However, I'm not willing to break language conventions, common use cases, and even the concept of a class itself, just to wedge in one opinion on how it could be done. Sadly, the existing proposal does all of this. As an alternative, I've offered my proposal-class-members, which after a little research into possibilities, I've recently revised. I've tried working out a babel plugin for it, but I find that between the highly integrated parser, and the content for the current proposal, I don't have enough understanding of how it works to add my proposal. In lieu of that, I've almost completed a SweetJS macro that implements it. I will release it when it is completed. |
@rolivo and @rdking The I think it should be based on existing concepts and ECMAScript components (include spec. types and design logic) to build a complete and clealy language experience that is consistent with traditional and typical OOP. This is the direction of my proposal. |
Stop the
class-fields
proposal! strong recommend!There is now a new proposal, no prefix '#', no FIELD, no newly concepts! please rate it.
private-property
implement and test, try it pls
Maybe we still have time to stop this disaster. say no accept! say no for
class-fields
proposal! see here #100 !History of new proposal named
private-property
:[2019.08.22]
[2019.08.29]
Thanks all.
The text was updated successfully, but these errors were encountered: