Proposal: Add Map.prototype.putIfAbsent

Andrea Giammarchi andrea.giammarchi at gmail.com
Mon Oct 15 14:43:10 UTC 2018


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.

On Mon, Oct 15, 2018 at 2:49 PM Sathya Gunasekaran <gsathya at chromium.org>
wrote:

> 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 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.
> >
>
> V8 implemented this[0] and then reverted it[1] because it didn't
> actually help with performance. IIRC, JSC still has this optimization.
>
> [0]:
> https://chromium.googlesource.com/v8/v8/+/3ca6408511900328e11175c38b128b3a6cebb7ec
> [1]:
> https://chromium.googlesource.com/v8/v8/+/a27daf04a36ead5484e7a09a447af918555006dc
>
> >
> >
> > On Thu, Oct 11, 2018 at 6:20 AM Jordan Harband <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>
> wrote:
> >>>
> >>> I presume you mean this?
> >>>
> >>> ```js
> >>> // 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> 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>
> wrote:
> >>>>>
> >>>>> Consider the following function
> >>>>>
> >>>>> ``` js
> >>>>>
> >>>>> /**
> >>>>>
> >>>>> * 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
> >>>>>
> >>>>> ``` js
> >>>>>
> >>>>> 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
> >>>>>
> >>>>> ``` js
> >>>>>
> >>>>> 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](
> https://api.dartlang.org/stable/2.0.0/dart-core/Map/putIfAbsent.html)
> >>>>>
> >>>>> - [Java] [Map.putIfAbsent](
> https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#putIfAbsent-K-V-
> )
> >>>>>
> >>>>> _______________________________________________
> >>>>> es-discuss mailing list
> >>>>> es-discuss at mozilla.org
> >>>>> https://mail.mozilla.org/listinfo/es-discuss
> >>>>
> >>>> _______________________________________________
> >>>> es-discuss mailing list
> >>>> es-discuss at mozilla.org
> >>>> https://mail.mozilla.org/listinfo/es-discuss
> >>
> >> _______________________________________________
> >> es-discuss mailing list
> >> es-discuss at mozilla.org
> >> https://mail.mozilla.org/listinfo/es-discuss
> >
> > _______________________________________________
> > es-discuss mailing list
> > es-discuss at mozilla.org
> > https://mail.mozilla.org/listinfo/es-discuss
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20181015/65fd9ce4/attachment-0001.html>


More information about the es-discuss mailing list