[SERVER-34281] ES6 arrow functions (lambdas) do not work with map-reduce Created: 03/Apr/18  Updated: 08/Jan/24  Resolved: 04/Feb/22

Status: Closed
Project: Core Server
Component/s: JavaScript, MapReduce
Affects Version/s: None
Fix Version/s: None

Type: Bug Priority: Major - P3
Reporter: James Kovacs Assignee: Backlog - Query Execution
Resolution: Gone away Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Related
related to SERVER-50614 JS 'Set' type does not serialize well... Backlog
related to SERVER-46243 Support ES6 Map object in MongoDB shell Backlog
related to SERVER-31551 Create a test which enumerates the ob... Closed
Assigned Teams:
Query Execution
Operating System: ALL
Steps To Reproduce:

var db = db.getSiblingDB('test');
db.lamdba_strangeness.insertMany([{a:1}, {a:2}, {a:3}]);
 
var mapFunction = function() { emit(this.ns, Object.bsonsize(this)); };
var mapLamdba = () => { emit(this.ns, Object.bsonsize(this)) };
 
var reduce = function(key, values) {
  return Array.sum(values);
};
 
var options = {
  out: {inline:1}
};
 
print("Using function() {}:");
printjson(db.lamdba_strangeness.mapReduce(mapFunction, reduce, options).results);
print("Using () => {}:");
printjson(db.lamdba_strangeness.mapReduce(mapLamdba, reduce, options).results);
 
db.lamdba_strangeness.drop();

Results:

$ mongo lamdba_strangeness.js
MongoDB shell version v3.6.3
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 3.6.3
Using function() {}:
[ { "_id" : null, "value" : 99 } ]
Using () => {}:
[ ]

Participants:

 Description   

Because ES6 arrow functions (aka lambdas) do not bind this (see Arrow functions), they do not work with the map-reduce framework in MongoDB.

For many people, lamdbas are seen as a more concise syntax and do not realize they behave differently. Either we should improve map-reduce to support ES6 arrow functions or clearly document that they are not supported (and why) in the map portion of map-reduce.



 Comments   
Comment by Esha Bhargava [ 04/Feb/22 ]

Closing these tickets as part of the deprecation of mapReduce.

Comment by Kevin Pulo [ 04/Nov/21 ]

This also affects the $function and $accumulator aggregation operators, no doubt because they also go via MozJSImplScope::invoke(). However, in this case, the lack of a this object is less relevant for $function and $accumulator, because (unlike $where and map-reduce) they always set this to an empty object ($function, $accumulator init, accumulate/merge, finalize).

What ends up happening, when an arrow function is used, is that the function isn't run (confirmed by adding a print() inside the function, and checking for jsPrint in the logs), and the value used in lieu of the function's return value is the actual lambda function itself (ie. as BSON Code type):

> db.test_arrow_fn.drop()
false
> db.test_arrow_fn.insert({})
WriteResult({ "nInserted" : 1 })
> db.test_arrow_fn.aggregate( [ { $project: { _id: {
    $function: {
        body: function(a,b,c) { return [ a, b, c ]; },
        args: [1,2,3],
        lang: "js" } } } } ] ).toArray()
[ { "_id" : [ 1, 2, 3 ] } ]
> db.test_arrow_fn.aggregate( [ { $project: { _id: {
    $function: {
        body: (a,b,c) => [ a, b, c ],
        args: [1,2,3],
        lang: "js" } } } } ] ).toArray()
[ { "_id" : { "code" : "(a,b,c) => [ a, b, c ]" } } ]
// But the expected output is the same as above, ie: [ { "_id" : [ 1, 2, 3 ] } ]
> 
> 
> db.test_arrow_fn.aggregate( [ { $group: { _id: 1, foo: {
    $accumulator: {
        init: function(a,b,c) { return [ "init", a, b, c ]; },
        initArgs: [1,2,3],
        accumulate: function(a,b,c) { return [ "acc", a, b, c ]; },
        accumulateArgs: [1,2,3],
        merge: function(a, b, c) { return [ "merge", a, b, c ]; },
        finalize: function(a, b, c) { return [ "finalize", a, b, c ]; },
        lang: "js" } } } } ] ).toArray()
[
        {
                "_id" : 1,
                "foo" : [
                        "finalize",
                        [
                                "acc",
                                [
                                        "init",
                                        1,
                                        2,
                                        3
                                ],
                                1,
                                2
                        ],
                        undefined,
                        undefined
                ]
        }
]
> db.test_arrow_fn.aggregate( [ { $group: { _id: 1, foo: {
    $accumulator: {
        init: (a,b,c) => [ "init", a, b, c ],
        initArgs: [1,2,3],
        accumulate: (a,b,c) => [ "acc", a, b, c ],
        accumulateArgs: [1,2,3],
        merge: (a, b, c) => [ "merge", a, b, c ],
        finalize: (a, b, c) => [ "finalize", a, b, c ],
        lang: "js" } } } } ] ).toArray()
[
        {
                "_id" : 1,
                "foo" : {
                        "code" : "(a, b, c) => [ \"finalize\", a, b, c ]"
                }
        }
]
> db.test_arrow_fn.aggregate( [ { $group: { _id: 1, foo: {
    $accumulator: {
        init: (a,b,c) => [ "init", a, b, c ],
        initArgs: [1,2,3],
        accumulate: (a,b,c) => [ "acc", a, b, c ],
        accumulateArgs: [1,2,3],
        merge: (a, b, c) => [ "merge", a, b, c ],
        finalize: function (a, b, c) { return [ "finalize", a, b, c ]; },
        lang: "js" } } } } ] ).toArray()
[
        {
                "_id" : 1,
                "foo" : [
                        "finalize",
                        {
                                "code" : "function() { return (a,b,c) => [ \"acc\", a, b, c ] }"
                        },
                        undefined,
                        undefined
                ]
        }
]
> 

Comment by Ian Whalen (Inactive) [ 13/Apr/18 ]

Yup, Query team agrees that we do want ES6 features to work in mapreduce and $where.

Comment by Max Hirschhorn [ 04/Apr/18 ]

Invoked through call or apply

Since arrow functions do not have their own this, the methods call() or apply() can only pass in parameters. thisArg is ignored.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#Invoked_through_call_or_apply

I suspect this means that the usage of SpiderMonkey's JS:Call() API in MozJSImplScope::invoke() to set the document as the this parameter doesn't have any effect. The behavior with using arrow functions in $where is more astonishing as it doesn't even appear that the function is ever called and it is instead being treated as a truthy value on its own.

> db.mycoll.find({$where: function() { throw new Error(tojson(this)); }})
Error: error: {
	"ok" : 0,
	"errmsg" : "Error: { \"_id\" : ObjectId(\"5ac455066740596e7935e29d\"), \"a\" : 1 } :\n@:1:22\n",
	"code" : 139,
	"codeName" : "JSInterpreterFailure"
}
> db.mycoll.find({$where: () => { throw new Error(tojson(this)); }})
{ "_id" : ObjectId("5ac455066740596e7935e29d"), "a" : 1 }

Perhaps the simplest path forward if we wanted to change the server's behavior would be to rewrite the ArrowFunctionExpression as its FunctionExpression equivalent as part of functionExpressionParser()? CC mira.carey@mongodb.com

Generated at Thu Feb 08 04:36:08 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.