String.from()

Now that we have .from() on Array, TypedArrays and Iterator it would seem logical to also have it on String.

Example:

String.from(function *() { yield 'a'; yield 'b' })` // "ab"

or

String.from({ length: 2 }, () => 'x')` // `"xx"`

I could even imagine something like:

String.from([65, 66])  // AB
1 Like

What problems would this solve?

You can do something similar today with fromCharCode

String.fromCharCode(...[65, 66]) // "AB"

I've always thought this was an obvious API to provide. I've written the implementation by hand maybe 10-20 times by now, and I'm still not sure if I'm doing it "right" from the perspective of perf. I'd much rather just focus on providing the iterable of characters and let the engine implementer decide what the best perf approach is for allocating out the string.

Mostly API consistency: People learning about .from noticing that String.from is a bit more complicated for no apparent. Reason.

const bytes = Uint8Array.from([65, 66])
const str = String.from(bytes)

My assumption would also be that it could be easy to improve the performance of this API (conversion of Uint8 → string).

String.fromCharCode(...[65, 66]) // "AB"

Unlike the current String.fromCharCode (thanks for mentioning it @senocular) there is no destructuring statement necessary to use String.from which should work in favor of speed.

Can you elaborate on a concrete use case or application where the speed of concatenating strings from an iterable (as opposed to from an array) is important?

Concatenating from an iterable is for writing comfort, . String.from(Iterator.range(65, 90)) in mind.

The performance aspect would be mostly in comparison to String.fromCharCode(). The existing one uses a list of arguments that is surely harder to optimize than a function call with the byte buffer directly passed to it. In comparison to (new TextDecoder()).decode(bytes) it is probably similar, but since String.from is so much more accessible, my thinking is that is probably going to be a preferred API.

Particularly for the performance-relevant TextDecoderStream, the following API - in an amendment to the initial idea - could be quite appealing.

await String.fromAsync(bytesStream)

instead of what I can gather is the fastest way to do that;

(await Array.fromAsync(bytesStream.pipeThrough(new TextDecoderStream())).join('')

String.fromCharCode(...Iterator.range(65, 90)) seems equally comfortable to me?

It is odd to me that you think it is equally comfortable.

  • String.fromCharCode(...X) is simply more to type than String.from(X).
  • String.fromCharCode is different from Array.from and Iterator.from which means one needs to remember this exception. (mental load)
  • fromCharCode suggests from a single character and not multiple, you'd have to read the signature to figure that out that it supports the fromCharCode(...) use.
  • Users don't learn all concepts at once. Some people might be familiar with Array.from(...) before learning about ... operator: they need to Learn (...) before they understand how to use this API.
  • fromCharCode does not support the second parameter String.from(input, mapFunction) which can come in handy at times.

In my daily programming, I barely need to concat strings from an iterator. I sometimes need to create a string from an array, in this case I will use array.join.

My understanding of the way ... in args works is that it takes a lazy data source and first makes it unlazy, i.e. allocates it. In this case it would have to allocate the structure into an array I think (any attempt to omit this step would change behavior), and only then once you'd make the complete array would you be able to throw that array out, using it as kind of temporary iterator for building the real allocation structure, the string. Why are we making an array to use as an iterator later when we have an iterator already right now?

That's a fair point if you're talking about a large number of graphemes or code points or code units, but on the scale of, like, "less than 5-6 zeros worth", I doubt the cost of a temporary array will even be noticeable (and if you're performance-sensitive you'd want to avoid the iterator protocol entirely anyways)

But this is at its core a matter of consistency more than it is a matter of perf. All these other APIs like Map constructor take iterables, and as far as I can tell if your argument were pervasively convincing they would only take arrays...

Now, that said, I'm in agreement that without a real use case this shouldn't go into the language.

The real use case I see is file streaming, but unfortunately sync iterators of characters don't allow for any asynchronicity while async iterators have too much overhead to step character-by-character. So in effect I guess with the state of the language as it is I am forced to agree with Jordan. Short of providing support for an iterable protocol performant enough to carry character streams, there would be little point in doing this