-
Notifications
You must be signed in to change notification settings - Fork 62
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
Record toString: useful, useless, or thrown(in implicit conversions) ? #136
Comments
I don't have a strong opinion on what On |
It would be possible if all records had an own Symbol.toStringTag property. |
About @ljharb 's "at the very least" suggestion, I think it would be a reasonable starting point to make ToString(record) === On the question of whether ToString on Records and Tuples should be useful: I've sat on this some more, and I definitely see the developer experience benefit--that's the goal here, really. Ideally, we'd get another operation in JavaScript that's more like
Thoughts? How should we make the consistency vs utility tradeoff? I was initially leaning towards "consistency" but now I feel myself leaning more towards "utility". |
Your suggestion seems like the best mix of both:
|
I agree this is important to preserve. I'd suggest that the explicit case be for the |
+1, I was initially thinking that it would output JSON.parse-able This would also generate a single line of "string representation". Does a better default format make sense, i.e. newlines and indenting object keys? |
I want to suggest we start by keeping it simple; prettyprinting gets very complicated to do well. Also note that my algorithm would still leave off the n on BigInts. I would leave fancier printing to DevTools or analogous libraries; this is just a default starting point for visibility. |
therefore it makes sense that
It's a really annoying quirk of JS that there's random toString implementations alll over the place. Let's avoid making this mistake for future additions. |
@Salmatron "annoying" is subjective, and I don't think that's universally agreed. It's actually quite annoying to me that null objects throw in a string context - to me, that is the mistake. |
Subjective, yes. I am of the opinion that developer mistakes should be pointed out early rather than swept under the rug. All objects without an explicit toString() implementation should throw IMO. How is '[object Object]' useful in any way? All that does is mask actual bugs. Putting a non-toString() type into a string context is a mistake that should be caught. (and Symbol.toStringTag should be read the proper way). Like Then there's array.sort() which casts everything to string. If Record has an expensive toString() method that dumps some JSON-like representation, that's gonna make things crawl on arrays with large data structures. |
What would the behavior of `Box(${String(content)})` What if the object contained in a const obj = {
toString() {
return `{${Object.entries.map(([key, value]) => `${key}: ${String(value)}`).join(', ')}}`;
}
};
obj.box = Box(obj);
String(obj); // ? |
Same thing a toString would do that has an infinite loop - halt the program. |
If we want to constrain access to |
That's a very good point I totally forgot about, and probably the biggest drawback to allowing virtualization through |
We are considering the option of throwing here for implicit conversions. This would have the effect of solving #289 by throwing on operators. If passing explicitely to the String constructor, we would prefer a useless string as anyone passing to that constructor could also pass it to JSON.stringify where they would have more control over the conversion. What do you think @ljharb? |
Why wouldn’t toString just defer to JSON.stringify, if toJSON ensures that JSON.stringify prints something useful? |
Tuples follow Arrays behavior for toString and JSON.stringify (following the general design theme of basing Tuple methods on Arrays). String( [1, [ [ [2]]], 3]); // '1,2,3'
String(#[1, #[#[#[2]]], 3]); // '1,2,3'
JSON.stringify( [1, [ [ [2]]], 3]); // '[1,[[[2]]],3]'
JSON.stringify(#[1, #[#[#[2]]], 3]); // '[1,[[[2]]],3]' It is potentially odd that a Tuple would serialize differently when passed to String(#[1, #[#{ t: 2 }]]); // `1,{"t": 2}`
String(#{ t: #[#[2]] }); // `{"t":[[2]]}` |
The usefulness is for debugging, and i don't think list-likes and dictionary-likes need to be consistent with each other. |
Summary post for reference ContextWithout explicit handling either by the application, or in the spec itself, treating a record as a string can't produce a useful result - in fact right now implicit conversion will throw an error. This is because Records do not have any of the methods that are used in the string conversion routine: const rec = #{ prop: 42 };
rec.toString === undefined;
Object.prototype.toString.call(rec); // `[object Record]`
String(rec); // [object Record]
JSON.stringify(rec); // `{"prop":42}`
const msg = `my record is: ${rec}`; // throws TypeError
// instead of `my record is: [object Record]` Returning const obj = { prop: 42 };
const set = new Set([1, 2, 3]);
console.log(`obj: ${obj} set: ${set}`); // "obj: [object Object] set: [object Set]" For this reason, when logging, it can be preferable to leave the stringification of values to the platform/library rather than performed at the individual call sites: $ node -e "console.log('logging:', { prop: 42 })"
logging: { prop: 42 } We expect a similar expierence for Records, so when passed directly to debugging mechanisms a rich representation is produced. ThrowingWe hypothesize that implicit conversion of a record to a string is a programming error based on the fact that the string would display no 'helpful' information about the record's contents. Similar to when someone accidentally gets There is some precedent for throwing on implicit string conversion of a primitive as this is what happens with If we imagine an application that captures logs during execution, perhaps it is a webserver. An issue is reported, when the developers go to check the logs around the time of the issue they see: A counter-argument to this is if the application only logs when there is an error, the log is in a A safer catch blockfunction safeHandler(originalError, handler) {
try {
return handler(originalError);
} catch (handlerError) {
if (handlerError === originalError) {
throw originalError;
} else {
throw new AggregateError([originalError, handlerError]);
}
}
}
try {
...
} catch (err) {
// the safeHandler ensures we don't lose `err` when our logging goes wrong
safeHandler(err, () => {
// whoops, this will throw when the record is converted to a string
console.log(`error during processing of record: ${record}`);
});
} A useful stringInstead of producing // Hypothetical:
const rec = #{ foo: #{bar: 42} };
`rec is ${rec}`; // `rec is #{"foo": #{"bar": 42}}` This is similar to languages like Python and Java from collections import namedtuple
Point = namedtuple('Point', 'x y')
pt1 = Point(1.0, 5.0)
print(f'pt1 is: {pt1}') # "pt1 is: Point(x=1.0, y=5.0)"
# String interpolation is different to string concatenation in Python:
'pt1 is: ' + pt1 # TypeError: can only concatenate str (not "Point") to str public class Main {
private record Point(float x, float y) { }
public static void main(String args[]) {
Point p = new Point(1.0f, 5.0f);
String message = String.format("p is %s", p);
System.out.println(message); // "p is Point[x=1.0, y=5.0]"
}
} The easiest (least amount of spec text) would be to reuse // Hypothetical JSON-inspired but bespoke string representation, that recursively stringifies its elements
const rec = #{ foo: #[undefined, NaN, Symbol("s"), 1n] };
`rec is ${rec}`; // `rec is #{"foo": #[undefined, NaN, Symbol(s), 1]}` The downsides to this are:
ConclusionWhile producing a "useful" string was an interesting thought experiment, the champion group's position is to retain consistency with default object behavior of returning the static string |
I don't think that consistency should be sought. If Objects hadn't made the mistake of being everything's base class, I think Object.prototype.toString would do exactly what we're asking for here. Object is special; things shouldn't strive to be consistent with it. |
I think this precedent is more recent than |
Map's purpose is to have object keys, and objects don't have a useful string serialization for the reasons discussed, so I think it's a consequence of the same precedent. Records can't have objects, so they're free of that constraint. |
Thinking purely as a user:
As such, I believe it is inappropriate to view this lopsidedly as an issue with Record and not with Tuple.
|
Thanks @rkirsling. How much of a risk do you think there would be if |
I would hope nobody is relying on array toString when join exists - and i doubt that anyone doing such a refactor would miss the difference. |
I agree with what @ljharb said, but moreover, I feel like the only reason we'd be aligning with Array to begin with is because its behavior is "just good enough" to accept. But it too has clear room for improvement, and if Record and Tuple are in clear alignment on a "better" solution, I think the divergence from Array would be enthusiastically forgiven. |
I agree that returning However, in the interest of developer ergonomics, I suggest a new method: While it doesn't avoid all of the downsides in acutmore's comment, since it's not called implicitly these are IMHO less dangerous. This also opens up the opportunity for adding a Naturally, I would make the same suggestion for a In any case, having a built-in way to represent Records (and Tuples) as strings is more useful than consistency in the form of |
I do see some value in having a nicer toString behavior for Records and Tuples, but it becomes a bit hard to limit the scope of such a project. In prettyprinters for data structures in several languages, there are mechanisms to limit the depth and the length traversed, as well as possibly control whitespace. This is exactly what |
PR #354 opened so implicit One small thing we would lose with this is that it's no longer an error if someone attempts to use a Record as an object property key. There is the potential to add a check to toPropertyKey that would throw if the type is record or tuple. So that would be one place where implicit conversion would still not be allowed. What do people think? |
I think a |
I was pretty surprised by the BigInt behavior. I shouldn’t have been — it does make sense — but somehow a ToString behavior I already knew to expect for BigInts on their lonesome seemed like a bug in the context of a serialization which (having no symbol values) would have roundtripped with one more character. It likewise surprised me that symbol members at any depth don’t just cause ToString to throw. An implicit ToString behavior introduced solely(?) as a debugging aid (vs one which incidentally happens to serve as one sometimes) seems pretty exotic to me. ToPrimitive/ToString are part of so many regular runtime contracts that aren’t related to debugging or introspection. I supposed I’d have imagined the “if we could go back...” story for O.p.toString would be to not have it in the first place rather than to deck it out. I may have missed some of the motivations here & these aren’t super carefully considered opinions or anything: this is just “field report” feedback based on exploring the proposal from the trying-to-use-it end. However if other folks do feel doubts about the currently specified behavior, I would suggest unilateral throwing behavior could be a reasonable MVP approach with the expectation that serialization be addressed in a ride-on proposal. My suspicion is that getting the full what-do-people-who-are-now-using-this-for-real-need-here feedback loop running before answering this question might produce different conclusions (perhaps different from anything which was discussed so far). |
Hi @bathos, thanks for the feedback. When the change was presented at the september plenary this year (slide), one thing we said is that we are purposefully not attempting to create an ad-hoc serialization format and that providing the detailed string is to help DX when the values are implicitly converted to strings. Previously we did throw an error, but the feedback we got was that this would not provide a good DX. As you say we could have special handling for bigint but symbols would still not work if people are intending a round-trip. If people do require serialization in the future this seems better left to a dedicated API rather than the |
@acutmore I wouldn’t want an ad-hoc serialization format, either. That’s how I’d describe what it is producing, though. |
Interesting. The reason we include the Do you think there are other ways we could produce a detailed string while making it as clear as possible that it is not intended to be a serialization format? |
Perhaps repurposing the bracketed O.p.toString “notation” to wrap the description “format”, like |
I don't think that's a reasonable restriction to have - functions' toString doesn't do that, for example. Just because we're not trying to provide a serialization mechanism doesn't mean that having one by accident is bad, or needs preventing. |
per #135 (comment)
At the very least, I'd expect Records to have a
Symbol.toStringTag
of"Record"
, which wouldObject.prototype.toString.call(record)
produce[object Record]
.However,
String(record)
,`${record}`
, etc, according to #135, will produce"[record]"
. This doesn't seem particularly useful at all; if someone wants to know it's a record, they'll typeof it.Objects have always had a useless toString, but since everything inherits from Object, it's a tough sell to come up with something broadly useful for it to do. Arrays' toString has problems, and could be much better if legacy didn't hold it back, but is still useful since it stringifies its contents. I would hope that Records can have a better user story around stringification than objects.
The text was updated successfully, but these errors were encountered: