Proposal: Add Map.prototype.putIfAbsent

# Man Hoang (5 years ago)

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

# Jordan Harband (5 years ago)

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?

# Isiah Meadows (5 years ago)

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.

# Jordan Harband (5 years ago)

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);

# Andrea Giammarchi (5 years ago)

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.

# Isiah Meadows (5 years ago)

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.

# Ron Buckton (5 years ago)

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

# Jack Lu (5 years ago)

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.

# Andrea Giammarchi (5 years ago)

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.

# Jack Lu (5 years ago)

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.

# Andrea Giammarchi (5 years ago)

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.

# Peter Hoddie (5 years ago)

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

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>
# Isiah Meadows (5 years ago)

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

# Andrea Giammarchi (5 years ago)

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.

# Sathya Gunasekaran (5 years ago)

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 this0 and then reverted it1 because it didn't actually help with performance. IIRC, JSC still has this optimization.

# Andrea Giammarchi (5 years ago)

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.