Proposal: Add Map.prototype.putIfAbsent
It seems like your proposed const value = map.putIfAbsent(key, getExpensiveValue);
is achievable already with const value = map.has(key) ? map.get(key) : map.set(getExpensiveValue());
- am I understanding your
suggestion correctly?
I presume you mean this?
// Proposed
map.putIfAbsent(key, init)
// How you do it now
let value
if (map.has(key)) {
value = map.get(key)
} else {
map.set(key, value = init())
}
BTW, I'd like to see this make it myself, just slightly different:
- How you call it:
map.getOrPut(key, init, thisValue=undefined)
- How
init
is called:init.call(thisValue, key, map)
This pattern is incredibly common for caching. It'd be nice if I didn't have to repeat myself so much with it. I would be more willing to stuff it in a utility function if it weren't for the fact the use cases often entail performance-sensitive paths, and engines aren't reliable enough in my experience with inlining closures passed to non-builtins.
Thanks, your correction explains what the benefit would be (the awkward
need to cache the value). However this seems simpler to me: if (!map.has(key)) { map.set(key, getValue()); } const value = map.get(key);
No. The raised concern has been common among developers and the main issue
is that set
returns itself which is the least useful pattern.
Your suggestion would make sense if const value = map.has(key) ? map.get(key) : map.set(key, createValue())
instead ES went for chain
ability and yet, to date, I have to see a single usage of map.set(a, 1).set(b, 2)
in the wild.
On top of that, needing to .has
and then .get
is a performance
shenanigan many would likely avoid at all costs 'cause AFAIK in no engine
.has(key)
temporarily retains last searched key to boost up the immediate
.get(key)
later on so having a .putIfAbsent(key, createValue())
that
returns either the found or the created value is a clear win.
It's slghtly simpler in terms of lines of code, but it's still more awkward than it should be for that kind of thing.
I have seen this in other languages as getOrCreate(key, valueFactory)
. Dictionaries in .NET have a TryGetValue(key, out value)
method that returns a boolean and has an ‘out’ parameter used to assign the value if it exists. The performance issue regarding keys is one of my common concerns with Set.prototype.add
, as it would have been significantly more useful for add
to return a Boolean indicating whether the value was added (true), or was already present (false).
At the end of the day, I usually just end up defining mapGetOrCreate(map, key, valueFactory)
and setAdd(set, value)
utility functions that I end up using.
From: es-discuss <es-discuss-bounces at mozilla.org> On Behalf Of Andrea Giammarchi
Sent: Wednesday, October 10, 2018 9:30 PM To: Jordan Harband <ljharb at gmail.com>
Cc: es-discuss at mozilla.org Subject: Re: Proposal: Add Map.prototype.putIfAbsent
No. The raised concern has been common among developers and the main issue is that set
returns itself which is the least useful pattern.
Your suggestion would make sense if const value = map.has(key) ? map.get(key) : map.set(key, createValue())
instead ES went for chain ability and yet, to date, I have to see a single usage of map.set(a, 1).set(b, 2)
in the wild.
On top of that, needing to .has
and then .get
is a performance shenanigan many would likely avoid at all costs 'cause AFAIK in no engine .has(key)
temporarily retains last searched key to boost up the immediate .get(key)
later on so having a .putIfAbsent(key, createValue())
that returns either the found or the created value is a clear win.
On Thu, Oct 11, 2018 at 6:20 AM Jordan Harband <ljharb at gmail.com<mailto:ljharb at gmail.com>> wrote:
Thanks, your correction explains what the benefit would be (the awkward need to cache the value). However this seems simpler to me: if (!map.has(key)) { map.set(key, getValue()); } const value = map.get(key);
On Wed, Oct 10, 2018 at 9:07 PM Isiah Meadows <isiahmeadows at gmail.com<mailto:isiahmeadows at gmail.com>> wrote:
I presume you mean this?
// Proposed
map.putIfAbsent(key, init)
// How you do it now
let value
if (map.has(key)) {
value = map.get(key)
} else {
map.set(key, value = init())
}
BTW, I'd like to see this make it myself, just slightly different:
- How you call it:
map.getOrPut(key, init, thisValue=undefined)
- How
init
is called:init.call(thisValue, key, map)
This pattern is incredibly common for caching. It'd be nice if I didn't have to repeat myself so much with it. I would be more willing to stuff it in a utility function if it weren't for the fact the use cases often entail performance-sensitive paths, and engines aren't reliable enough in my experience with inlining closures passed to non-builtins.
On Wed, Oct 10, 2018 at 22:54 Jordan Harband <ljharb at gmail.com<mailto:ljharb at gmail.com>> wrote:
It seems like your proposed const value = map.putIfAbsent(key, getExpensiveValue);
is achievable already with const value = map.has(key) ? map.get(key) : map.set(getExpensiveValue());
- am I understanding your suggestion correctly?
On Wed, Oct 10, 2018 at 12:46 AM Man Hoang <jolleekin at outlook.com<mailto:jolleekin at outlook.com>> wrote:
Consider the following function
/**
* Parses the locale sensitive string [value] into a number.
*/
export function parseNumber(
value: string,
locale: string = navigator.language
): number {
let decimalSeparator = decimalSeparators.get(locale);
if (!decimalSeparator) {
decimalSeparator = Intl.NumberFormat(locale).format(1.1)[1];
decimalSeparators.set(locale, decimalSeparator);
}
let cleanRegExp = regExps.get(decimalSeparator);
if (!cleanRegExp) {
cleanRegExp = new RegExp(`[^-+0-9${decimalSeparator}]`, 'g');
regExps.set(decimalSeparator, cleanRegExp);
}
value = value
.replace(cleanRegExp, '')
.replace(decimalSeparator, '.');
return parseFloat(value);
}
const decimalSeparators = new Map<string, string>();
const regExps = new Map<string, RegExp>();
This function can be simplified quite a bit as follows
export function parseNumber(
value: string,
locale: string = navigator.language
): number {
const decimalSeparator = decimalSeparators.putIfAbsent(
locale, () => Intl.NumberFormat(locale).format(1.1)[1]);
const cleanRegExp = regExps.putIfAbsent(
decimalSeparator, () => new RegExp(`[^-+0-9${decimalSeparator}]`, 'g'));
value = value
.replace(cleanRegExp, '')
.replace(decimalSeparator, '.');
return parseFloat(value);
}
if Map
has the following instance method
export class Map<K, V> {
/**
* Look up the value of [key], or add a new value if it isn't there.
*
* Returns the value associated to [key], if there is one.
* Otherwise calls [ifAbsent] to get a new value, associates [key] to
* that value, and then returns the new value.
*/
putIfAbsent(key: K, ifAbsent: () => V): V {
let v = this.get(key);
if (v === undefined) {
v = ifAbsent();
this.set(key, v);
}
return v;
}
}
Java's Map has a putIfAbsent
method, which accepts a value rather than a function for the second parameter. This is not ideal as computing the value may be expensive. A function would allow the value to be computed lazily.
References
- [Dart] Map.putIfAbsent
- [Java] Map.putIfAbsent
Don't you think the name putIfAbsent
is somewhat misleading? In the sense that the purpose of this function is to get the value, with a side effect of setting a default value when it's absent.
I think it boils down to the fact .put(value)
is a well known method
outside the JS world that returns the value.
I honestly would love to have .put
in Map, Set, and Weak friends, so
that the whole thing would be:
const value = map.has(key) ? map.get(key) : map.put(key, createValue());
// with the more than common safe shortcut as
const value = map.get(key) || map.put(key, createValue());
If you understand that put
by default returns the value you are putting,
putIfAbsent
makes perfect sense as a method that put
the value only if
absent but like put
would return either the present one, or the one
stored 'cause absent.
It makes sense for a put
to return the value, but it's awkward to say put
when you want to get a value.
And I'm fond of your proposal of put
.
I think having both set
and put
would be actually awesome, we have
substr
and slice
as well that do slightly different things with
slightly different names and there are valid use cases/simplification for
both string methods.
AFAIK Allen mentioned it'd be awkward to have put too, but I hope others agree it would solve most issues developers have with constants and one/off WeakMap setups which is the most common use case with weak maps, weak sets, and not weak counterparts.
we have
substr
andslice
as well that do slightly different things with slightly different names and there are valid use cases/simplification for both string methods
substr is in Annex B for web compatibility.
The specification notes:
All of the language features and behaviours specified in this annex have one or more undesirable characteristics and in the absence of legacy usage would be removed from this specification
And:
These features are not considered part of the core ECMAScript language. Programmers should not use or assume the existence of these features and behaviours when writing new ECMAScript code. ECMAScript implementations are discouraged from implementing these features unless the implementation is part of a web browser or is required to run the same legacy ECMAScript code that web browsers encounter.
Our XS engine for emebdded followed this guidance: we didn't implement strstr for a long time. Alas, developers from the web are so accustomed to having it that we eventually gave in and provided it. Some conformance details here:
https://github.com/Moddable-OpenSource/moddable/blob/public/documentation/xs/XS%20Differences.md#conformance <https://github.com/Moddable-OpenSource/moddable/blob/public/documentation/xs/XS%20Differences.md#conformance>
I personally wish that'd be moved into the core standard simply because it's easier in a lot of cases (when you're doing offset + length instead of start/end). But I'm not really that opinionated over it.
Isiah Meadows contact at isiahmeadows.com, www.isiahmeadows.com
You slightly missed my point Peter ... put and set are completely different and it wouldn't be awkward to have them both, neither I'm suggesting to put in annex b set or put: both have use cases, both are valid, both add values, both are different.
On Thu, Oct 11, 2018 at 6:29 AM Andrea Giammarchi <andrea.giammarchi at gmail.com> wrote:
No. The raised concern has been common among developers and the main issue is that
set
returns itself which is the least useful pattern.Your suggestion would make sense if
const value = map.has(key) ? map.get(key) : map.set(key, createValue())
instead ES went for chain ability and yet, to date, I have to see a single usage ofmap.set(a, 1).set(b, 2)
in the wild.On top of that, needing to
.has
and then.get
is a performance shenanigan many would likely avoid at all costs 'cause AFAIK in no engine.has(key)
temporarily retains last searched key to boost up the immediate.get(key)
later on so having a.putIfAbsent(key, createValue())
that returns either the found or the created value is a clear win.
V8 implemented this0 and then reverted it1 because it didn't actually help with performance. IIRC, JSC still has this optimization.
For what I could test, map.has(any)
followed by map.get(any)
is already
very performant in both Chrome and Safari but having mep.set(any, value)
returning map
instead of value
is still annoying for most common use
cases.
Consider the following function
/** * Parses the locale sensitive string [value] into a number. */ export function parseNumber( value: string, locale: string = navigator.language ): number { let decimalSeparator = decimalSeparators.get(locale); if (!decimalSeparator) { decimalSeparator = Intl.NumberFormat(locale).format(1.1)[1]; decimalSeparators.set(locale, decimalSeparator); } let cleanRegExp = regExps.get(decimalSeparator); if (!cleanRegExp) { cleanRegExp = new RegExp(`[^-+0-9${decimalSeparator}]`, 'g'); regExps.set(decimalSeparator, cleanRegExp); } value = value .replace(cleanRegExp, '') .replace(decimalSeparator, '.'); return parseFloat(value); } const decimalSeparators = new Map<string, string>(); const regExps = new Map<string, RegExp>();
This function can be simplified quite a bit as follows
export function parseNumber( value: string, locale: string = navigator.language ): number { const decimalSeparator = decimalSeparators.putIfAbsent( locale, () => Intl.NumberFormat(locale).format(1.1)[1]); const cleanRegExp = regExps.putIfAbsent( decimalSeparator, () => new RegExp(`[^-+0-9${decimalSeparator}]`, 'g')); value = value .replace(cleanRegExp, '') .replace(decimalSeparator, '.'); return parseFloat(value); }
if
Map
has the following instance methodexport class Map<K, V> { /** * Look up the value of [key], or add a new value if it isn't there. * * Returns the value associated to [key], if there is one. * Otherwise calls [ifAbsent] to get a new value, associates [key] to * that value, and then returns the new value. */ putIfAbsent(key: K, ifAbsent: () => V): V { let v = this.get(key); if (v === undefined) { v = ifAbsent(); this.set(key, v); } return v; } }
Java's Map has a
putIfAbsent
method, which accepts a value rather than a function for the second parameter. This is not ideal as computing the value may be expensive. A function would allow the value to be computed lazily.References