Proposal: Add Map.prototype.putIfAbsent

Ron Buckton Ron.Buckton at microsoft.com
Thu Oct 11 04:49:46 UTC 2018


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?

```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<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
``` 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<https://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fapi.dartlang.org%2Fstable%2F2.0.0%2Fdart-core%2FMap%2FputIfAbsent.html&data=02%7C01%7Cron.buckton%40microsoft.com%7C80d5a89a1ce449e3fac508d62f32324e%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C636748289999447872&sdata=VsBS5LsqzgSF%2BFylqsGDM4uCqUj%2FG%2BYqtbuFLPhEaKU%3D&reserved=0>)
- [Java] [Map.putIfAbsent](https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#putIfAbsent-K-V-<https://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fdocs.oracle.com%2Fjavase%2F8%2Fdocs%2Fapi%2Fjava%2Futil%2FMap.html%23putIfAbsent-K-V-&data=02%7C01%7Cron.buckton%40microsoft.com%7C80d5a89a1ce449e3fac508d62f32324e%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C636748289999447872&sdata=gkh4l7LiTCJ0L8%2BGYbVsxlhvmlQYwmK9ZGxkTdhyM1A%3D&reserved=0>)
_______________________________________________
es-discuss mailing list
es-discuss at mozilla.org<mailto:es-discuss at mozilla.org>
https://mail.mozilla.org/listinfo/es-discuss<https://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fmail.mozilla.org%2Flistinfo%2Fes-discuss&data=02%7C01%7Cron.buckton%40microsoft.com%7C80d5a89a1ce449e3fac508d62f32324e%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C636748289999447872&sdata=AT2igJX94CnydPQ8daMnVFKq3c3nCQe1fwYxfZOqNXU%3D&reserved=0>
_______________________________________________
es-discuss mailing list
es-discuss at mozilla.org<mailto:es-discuss at mozilla.org>
https://mail.mozilla.org/listinfo/es-discuss<https://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fmail.mozilla.org%2Flistinfo%2Fes-discuss&data=02%7C01%7Cron.buckton%40microsoft.com%7C80d5a89a1ce449e3fac508d62f32324e%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C636748289999447872&sdata=AT2igJX94CnydPQ8daMnVFKq3c3nCQe1fwYxfZOqNXU%3D&reserved=0>
_______________________________________________
es-discuss mailing list
es-discuss at mozilla.org<mailto:es-discuss at mozilla.org>
https://mail.mozilla.org/listinfo/es-discuss<https://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fmail.mozilla.org%2Flistinfo%2Fes-discuss&data=02%7C01%7Cron.buckton%40microsoft.com%7C80d5a89a1ce449e3fac508d62f32324e%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C636748289999447872&sdata=AT2igJX94CnydPQ8daMnVFKq3c3nCQe1fwYxfZOqNXU%3D&reserved=0>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20181011/298a9569/attachment-0001.html>


More information about the es-discuss mailing list