introduction of statefull functions (inspired by Scala language) - advanced specification of "operator()" for arbitrary objects

# Igor Baklan (9 years ago)

It would be nice to "bring sense" to expressions like obj(a1, ... , aN) = val. (like obj(x) = y) In Scala langue it defined in pretty clear and simple way:

   "obj(a1, ... , aN)" <==> "obj.apply(a1, ... , aN)"
   "obj(a1, ... , an) = val" <==> "obj.update(a1, ... , aN, val)"

Of course this applied only to that cases when obj was not defined like method (in case of regular methods mth(args) = val will cause compile time error). So in Scala even arrays and maps are accessed and updated using "()" operator

   js( arr[index] = val ) <==> scala( arr(index) = val )

(see www.scala-lang.org/api/2.10.0/index.html#scala.Array , docs.scala-lang.org/overviews/collections/maps#operations-in-class-mutablemap , etc)

So the proposals are:

(1) to introduce symbols like @@apply and @@update with very similar to Scala meaning (for cases when obj.sttFunc is not a function)

  "obj.sttFunc(a1, ... , aN) = val" ==> "obj.sttFunc[Symbol.update] (obj.sttFunc, obj, [a1, ... , aN], val)"
  "obj.sttFunc(a1, ... , aN)" ==> "obj.sttFunc[Symbol.apply] (obj.sttFunc, obj, [a1, ... , aN])"

(2) to extend Prxoy object specification to support update action along with apply action:

I would like to write something similar to

var target = {
   jsApply: function(key){
      console.log("apply: ", key);
      return key;
   },
   jsUpdate: function(key, val){
      console.log("update: (", key, ") = ", val);
      return val;
   }
};
var sttFunc = new Proxy(target, {
   apply: function(target, thisArg, argumentsList) {
      target.jsApply.apply(thisArg, argumentsList);
   },
   update:  function(target, thisArg, argumentsList, assignValue) {
      target.jsApply.apply(thisArg, Array.from(argumentsList).concat([assignValue]));
   }
});

And then run it as following

> sttFunc ("key") = "value";

update: ( key ) =  value

> sttFunc ("key");

apply:  key
"key"
# Jason Orendorff (9 years ago)

The nice thing about @@symbol-named methods is that we don't have to extend the Proxy API for them. They're just method calls.

This is why there's no hasInstance Proxy trap. instanceof basically boils down to a method call, and we already have Proxy traps for method calls: get and apply.

It's a good thing, too, because adding a new Proxy trap is a last resort. Just from a conceptual standpoint, the less complicated objects are, the better; just because there are 14 fundamental operations on objects doesn't mean we're eager to add more. But there's also an inherent compatibility issue. Whenever you add a new trap, the deal is, all existing Proxy handlers were written without consideration for the new operation. So old code, used in combination with the new language feature, would tend to break.

# Isiah Meadows (9 years ago)

I would like to point out that this particular example could be done in terms of existing proxies now:

var sttFunc = new Proxy({}, {
    get(target, key) {
        console.log("get: [", key, "]");
        return key;
    },
    set(target, key, value) {
        console.log("set: [", key, "] = ", val);
        return true; // success
    }
});

// Usage:
sttFunc["key"] = "value"; // set: [key] = value
sttFunc["key"]; // get: [key]
# Igor Baklan (9 years ago)

Fist of all little bit more details. Here @@apply and @@update proposed like some "fallback scenario" for "()" for non function objects (and not Proxy objects) - generally for objects that do not support "()" "natively".

(1) Currently js has "()" operation (lets call it "_=_()") but has not "()=" operation (lets call it "_()=_"). Also operation "()" currently applied only to a functions, but not applied to the arbitrary objects (even when I create Proxy from arbitrary object and trying to implement "()" in Proxy, it still not work since target object was not a function, as for me it is more bug then feature:) ). So it supposed that if obj is not a function, then operation "()" goes to fallback scenario and try to use obj[@@apply] as an implementation of "()" operation for this obj object. But if obj object is Proxy object, than it obtains this operation as "direct" call of apply handler method (bypassing fallback to obj[@@apply]) . So obj[@@apply] should be used for "()" operation in some similar way as vlaueOf and toString are used by "+" operation - when object itself natively support this operation (like Number, String, etc does) the no fallback scenarios are used - "+" operation just do its native implementation, but when object do not natively support "+" (case of arbitrary object, not Number, not String, ...) then some additional fallback actions happens (with calling valueOf / toString). So here is the same idea - if obj is a function (or Proxy object) no fallback actions should occur - operation "()" should be done "natively". if object not a function neither a Proxy object, then fallback to obj[@@apply] should occur (and signature of this obj[@@apply]-method assumed to be the same as in proxy handler apply method (function(target, thisArg, argumentsList) {}) - so it should obtain more information then just a list of an arguments it also should obtain target and thisArg where target should be bound to obj and thisArg should be also properly passed, if it was owner.obj(arg) then thisArg should be bound to owner (NOT to obj)).

(2) For "()=" operation (obj(..args) = val) currently no implementation present (this construction unconditionally cause compile time error). But if we assume that it really present/implemented, and name it as update (like it was done in Scala), then update and apply in Proxy handler should be in the same relationship as get and set , and once again, it should be assumed that Prxoy object may "natively" implement update operation, and "fallback scenario" related to obj[@@update] should NOT happen for Proxy objects.

So more strict definition off behavior will look like this: Considering owner.obj(..args) = val case:

  1. If owner.obj is NOT a Proxy object (do not support _()=_ operation "natively") then this should be evaluated to
owner.obj[@@update](/*target:*/ owner.obj, /*thisArg:*/ owner, args)
  1. If owner.obj is Proxy object (and that's why do support _()=_ operation "natively") then underling assignment should refer to Proxy object handler update operation with same arguments as above, if proxy handler do not support update implementation, than it can fallback to target implantation of _()=_ operation (and apply this rule recursively but this time for target)

Considering this thoughts particular answer to your post will be:

... and we already have Proxy traps for method calls: get and apply

if obj is Proxy object, it assumed that it implements "_=_()" and "_()=_" operations "natively" so it should not use obj[@@apply] and obj[@@update] methods (and that is why get should not trigger), instead "_=_()" and "_()=_" operations should be directly forwarded to proxy handler (apply and update method accordingly) ; obj[@@apply] and obj[@@update] methods should be used only for objects which are both not js Function neither js Proxy object.

Also proposed update handler method is more like counterpart of apply, like set proxy handler method is counterpart to get proxy handler method.

But there's also an inherent compatibility issue. Whenever you add a new

trap, the deal is, all existing Proxy handlers were written without consideration for the new operation. So old code, used in combination with the new language feature, would tend to break.

In case if old code was used, then it should mean that no update method present in proxy handler and so that obj(...args) = val will fallback to target(...args) = val (by actual result of execution); if target object was also defined/constructed by old code, then construct obj(...args) = val will lead to the same error as target(...args) = val does (since old code will not implement it for target too), so nothing will change for old code in this case; if old code of proxy will be applied for target object from "new code" (that occasionally happen to implement "_()=_" operation some how), then obj(...args) = val will succeed transparently and has same result as target(...args) = val. And yes, "old code" proxy handler will not intercept this situation, and that means that all handlers that currently intercept all operations over object will became less "universal". But this doesn't mean that "purely old" code will be broken in any way by introduction of this feature.

# Igor Baklan (9 years ago)

I would like to point out that this particular example could be done in

terms of existing proxies ...

Yes, for this simplistic case that you properly mentioned, operation [] is enough, but when i writing that example I rather keep in mind js Map and object keys. So the goal is also simplified syntax of accessing and mutating maps, like:


var map = new Map();
var key = {foo: "bar"};
var val = {some: "value"};

map(key) = val; // same as map.set(key, val) if js has "`_()=_`" operation
console.assert(map(key) === val); // same as (map.get(key) === val)
map.delete(key);
console.assert(map(key) == null);

If "_()=_" operation will be available in language, then anyone will be able to implement his own MultiDimMap multidimensional map (on top of existing single-dimensional map), with usage similar to following


var map = new MultiDimMap();
var key0 = {foo: "bar"};
var key1 = {foo1: "bar1"};
var val = {some: "value"};

map(key0, key1) = val;
console.assert(map(key0, key1) === val);
map.delete(key0, key1);
console.assert(map(key0, key1) == null);

Also to provide more complete information about current state of similar feature in other languages, it would be good to take a look on C# indexers. It also provides extensive possibility to implement "_[]=_" operation with arbitrary number of arguments, so in case of C# expressions like obj.idxr[arg1, arg2, ... , argN] = val is pretty valid (if obj.idxr indexer properly implemented in obj). But this feature looks for me more ambiguous with current implementation of Proxy object (more specifically with current definition of set and get methods of proxy handler). In particular set states signature:

set: function(target, property, value, receiver)

but if we will have a list of arguments (like obj[arg1, ... , argN]) then what should be passed to property argument, if it will be property == [arg1, ... , argN] then it looks like OK, but if we pass only one argument, like obj[Array.from([arg1, ... , argN])] what should be in property , of course it should be rather property == [ [arg1, ... , argN] ] but still it looks little bit ambiguous.

So for me Scala approach for "indexing" with "()" / "()=" operations looks more nice.

(More information about Scala approach to apply/update - "()" / "()=" can be found at www.scala-academy.com/tutorials/scala-apply-update-methods-tutorial, also about maps: www.safaribooksonline.com/library/view/scala-cookbook/9781449340292/ch11s15.html, docs.scala-lang.org/overviews/collections/maps#operations-in-class-mutablemap )