Proposal: Symbol.inObject well-known symbol to customize the "in" operator's behavior

# Tom Barrasso (5 years ago)

Like Symbol.hasInstance but for the "in" operator. This symbol would work for both native and user-defined objects.

Example implementation prototyping native object:

String.prototype[Symbol.inObject] =
  function(searchString) {
    return this.includes(searchString)
}

Example implementation for user-defined object:

function range(min, max) => ({
    [Symbol.inObject]: (prop) => {
        return (prop >= min && prop <= max)
    }
})

Example usage:

("foo" in "food")    // true
(14 in range(1, 25)) // true
# Claude Pache (5 years ago)

Le 9 mai 2019 à 20:52, Tom Barrasso <tom at barrasso.me> a écrit :

Like Symbol.hasInstance but for the "in" operator. This symbol would work for both native and user-defined objects.

Example implementation prototyping native object:

String.prototype[Symbol.inObject] =
  function(searchString) {
    return this.includes(searchString)
}

Example implementation for user-defined object:

function range(min, max) => ({
    [Symbol.inObject]: (prop) => {
        return (prop >= min && prop <= max)
    }
})

Example usage:

("foo" in "food")    // true
(14 in range(1, 25)) // true

Those two examples seem to give to the in operator a meaning that it was not intended to have. The in operator is specifically meant to check whether a given property exists in a given object.

Also, there already exists a way to customise the behaviour of the in operator, namely by using a Proxy.

# Tom Barrasso (5 years ago)

Thanks interesting, I hadn’t realized it was possible to “trap” the in operator using Proxy. I may be wrong, but I don’t think Proxy is capable of operating on the prototype chain. Specifically, I don’t think you can change the behavior of the in operator for all Strings (which I’m sure many would prefer).

If this Symbol were seriously considered I believe it would expand the meaning of the in operator as you’re correct, this is definitely not it’s current intention.

Tom

# Isiah Meadows (5 years ago)

There's several ways proxies can change the result of in:

  • Directly via handler.has(target, key) (returns boolean)
  • Indirectly via handler.getOwnPropertyDescriptor(target, key) (returns existing descriptor or undefined if missing)
  • Indirectly via `handler.getPrototypeOf(target)' (returns relevant prototype)

The proxy trap has is almost a direct substitute for your Symbol.inObject in most cases, provided you're okay with keys getting stringified.

# Tom Barrasso (5 years ago)

handler.has(target, key) appears to be the most direct way to accomplish this, although as mentioned in requires dealing with string parsing. Here's a quick example I put together using Proxy to accomplish the range function.

function range(min, max) {
  return new Proxy(Object.create(null), {
    has (target, key) {
      return (Number(key) >= min && Number(key) <= max)
    }
  })
}

However, the same cannot be achieved easily for Strings. I consider using Strings directly, but got a "Uncaught TypeError: Cannot create proxy with a non-object as target or handler." I also considered using template tags as well, but that seemed like more of a novel hack and not advantageous compared to a simple function. This is the best I could come up with.

function q(str) {
  const S = class extends String{}
  return new Proxy(new S(str), {
    has (target, searchString) {
      return str.includes(searchString)
    }
  })
}

A class that extends String is sufficient where a String itself is not, although this has ramifications elsewhere like being passed by reference, not value, through functions. Based on the stringification and inability to work with native object prototype, I'm not fully convinced that Proxy is a complete substitute for Symbol.inObject.

Tom

# guest271314 (5 years ago)
"" in q("food") // true

Is the purpose of the code to return a boolean for string and integer input?

What is the significance of in operator usage to output?

# Claude Pache (5 years ago)

Le 9 mai 2019 à 23:17, Tom Barrasso <tom at barrasso.me> a écrit :

If this Symbol were seriously considered I believe it would expand the meaning of the in operator as you’re correct, this is definitely not it’s current intention.

The in operator has a well-defined meaning, that by design you can’t ignore even with the fanciest proxy. (And no, there is no hope for introducing a new mechanism that allows to overcome those limitations, since they are by design.) Consider for example the following proxy around a String object:

function conflateInAndIncludes(str) {
    return new Proxy(Object(str), { 
        has(target, key) { return str.includes(key) } 
    })
}

var FrankensteinFood = conflateInAndIncludes("food");

"foo" in FrankensteinFood // true, yeah!
"bar" in FrankensteinFood // false, yeah!
"length" in FrankensteinFood // TypeError: proxy can't report a non-configurable own property '"length"' as non-existent
# Tom Barrasso (5 years ago)

There are obvious limitations given the masking/ overwriting of the default implementation, but are these limitations not also true for other symbols like Symbol.hasInstance? If overwritten (including in a prototype chain), it then masks the original behavior of the instanceof operator. Symbols are one way to do this more explicitly and without the caveats of needing to use a Proxy everywhere.

Any symbol that allows for reconfiguring/ overloading an operator risks breaking the assumptions about how that operator is supposed to behave. Perhaps I’m not understanding some way this is different/ more significant for overloading the in operator than say instanceof or the spread operator?

Perhaps a more reasonable use case than prototyping String would be a user-defined, iterable data structure like an unrolled linked list? Even with Proxy this is not possible unless we wrapped all constructor calls.

class UnrolledLinkedList {
  constructor(nodeCapacity = 8) {
    // ...
  }

  [Symbol.inObject](index) {
    return (index >= 0 && index <= this.length)
 }

let list = new UnrolledLinkedList();
list.add(1);
list.add(2);
list.add(3);

if (1 in list) {
  // ...
}

A developer may want to mimic the behavior of Array, but using a Proxy would require wrapping all constructor calls or calls to the in operator. The other alternative is to set numeric properties on the list, but doing so isn't always possible for every data structure.

Tom

# guest271314 (5 years ago)

Still not gathering the significance or purpose of using in to generate a boolean output. What appears to be the requirement can be achieved by using Set or Map

let m = new Map([[0, "a"],[ 1, 1], [2, {}]]);
m.has(0); // true
m.get(2) // {}
# Isiah Meadows (5 years ago)

This example could be fixed by passing {} as the proxy target.