J@ArangoDB

{ "subject" : "ArangoDB", "tags": [ "multi-model", "nosql", "database" ] }

Using ES6 Features in ArangoDB

ArangoDB 2.4 will be shipped with an updated version of V8.

The V8 version included in 2.4 will be 3.29.59. This version will replace the almost two year old 3.16.14. A lot of things happened in V8 since then, and a lot of ES6 features have been added to it.

ES6 is not finalized yet, and support for it is a work in progress on all platforms.

ES6 provides many cool features that can make JavaScript developer’s life easier. In this blog post, I’ll summarize a few ES6 features that are available in ArangoDB, either for scripting purposes in the ArangoShell, or in your server-side Foxx actions inside the database.

I don’t want to put you off until Doomsday. ArangoDB 2.4 should be released next week. Time to play with some ES6 features!

Summary for the impatient

The following ES6 features are available in ArangoDB 2.4 by default:

  • iterators
  • the of operator
  • symbols
  • predefined collections types (Map, Set etc.)
  • typed arrays

Many other ES6 features are disabled by default, but can be made available by starting arangod or arangosh with the appropriate options:

  • arrow functions
  • proxies
  • generators
  • String, Array, and Number enhancements
  • constants
  • enhanced object and numeric literals

To activate all these ES6 features, start arangod or arangosh with the following options:

arangosh --javascript.v8-options="--harmony --harmony_generators"

Activating ES6 features

Work on ES6, also dubbed Harmony or ES.next, is still in progress. At the time of this writing, the ES6 specification was still in draft status.

Therefore no platform has implemented all ES6 features yet. And because ES6 is still a moving target, the already implemented features should still be considered experimental.

This is true for all environments that implement ES6 features. For example, Firefox and other browsers contain lots of experimental ES6 features already, providing a notice that these might change in future versions.

V8 is no exception here. It has turned most ES6 features off by default, but it provides several command-line options to turn them on explicitly.

The V8 version used for ArangoDB 2.4 provides the following ES6-related switches:

  • --harmony_scoping (enable harmony block scoping)
  • --harmony_modules (enable harmony modules (implies block scoping))
  • --harmony_proxies (enable harmony proxies)
  • --harmony_generators (enable harmony generators)
  • --harmony_numeric_literals (enable harmony numeric literals (0o77, 0b11))
  • --harmony_strings (enable harmony string)
  • --harmony_arrays (enable harmony arrays)
  • --harmony_arrow_functions (enable harmony arrow functions)
  • --harmony_classes (enable harmony classes)
  • --harmony_object_literals (enable harmony object literal extensions)
  • --harmony (enable all harmony features (except proxies))

These switches are all off by default. To turn on features for either arangod or arangosh, start it with the V8 option(s) wrapped into the ArangoDB option --javascript.v8-options, e.g.:

arangosh --javascript.v8-options="--harmony_proxies --harmony_generators --harmony_array"

On a side note: node.js is also using V8. Turning on ES6 features in node.js almost works the same way. Just omit the surrounding --javascript.v8-options="...":

node --harmony_proxies --harmony_generators --harmony_array

Note that the V8 options can only be set for the entire process (i.e. arangosh, arangod or node.js), and not just for a specific script or application. In reality this shouldn’t be too problematic as the vast majority of ES6 features is downwards-compatible to ES5.1.

ES6 features by example

Following I have listed a few select ES6 features that are usable in ArangoDB 2.4, in no particular order. I have omitted a few ES6 features that aren’t supported in bundled V8 version, and also omitted classes and modules due to lack of time.

Arrow functions

ES6 provides an optional arrow function syntax. The arrow function syntax is a shorthand for writing a full-blown function declaration. Here’s an example:

a simple arrow function
1
2
3
4
5
/* defines function pow */
var pow = (value => value * value);

/* calls pow */
pow(15);  /* 225 */

Arrow functions can also take multiple parameters. The syntax then becomes:

arrow function with multiple parameters
1
2
3
4
5
/* defines function sum */
var sum = (a, b) => a + b;

/* calls sum */
sum(3, 7);  /* 10 */

So far we have only seen arrow functions with simple expressions, but arrow function bodies can also be more complex and can contain multiple statements:

more complex arrow functions
1
2
3
4
5
6
7
8
9
10
11
12
13
var translations = {
  "en" : "English",
  "fr" : "French",
  "de" : "German"
};

/* using multiple statements */
["en", "fr", "xx"].map(value => {
  if (translations.hasOwnProperty(value)) {
    return translations[value];
  }
  return "unknown!";
});

Arrow functions are turned off by default. To enable them in arangod or arangosh, start them with the option --javascript.v8-options="--harmony_arrow_functions".

Maps and sets

ES6 maps provide a specialized alternative to regular objects in case a lookup-by-key functionality is required.

When no maps are available, storing keys mapped to objects is normally done using a plain object. With ES6, the same use case can be handled with using a Map object:

using an ES6 Map object
1
2
3
4
5
6
var m = new Map();

/* set 5M keys */
for (var i = 0; i < 5000000; ++i) {
  m.set("test" + i, i);
}

ES6 maps can be more efficient than plain objects in some cases. For the above case of storing 5M entries, an ES6 map is about twice as fast as a plain object on my laptop. Though there might be cases we are plain objects are still faster.

There’s more to ES6 Maps than just efficiency:

  • ES6 maps provide a member size which keeps track of the number of objects in the map. This is hard to achieve with a plain object.
  • Objects can only have string keys, whereas map keys can have different key types.
  • They don’t inherit keys from the prototype, so there is no hasOwnProperty hassle with Maps.

ES6 also comes with a specialized Set object. The Set object is a good alternative to plain JavaScript objects when the use case is to track unique values. Using a Set is more intuitive, potentially more efficient and may require even less memory than when implementing the same functionality with a plain JavaScript object:

using an ES6 Set object
1
2
3
4
5
6
var s = new Set();

/* set 5M values */
for (var i = 0; i < 5000000; ++i) {
  s.add("test" + i);
}

Maps and sets are enabled by default in arangod and arangosh. No special configuration is needed to use them in your code.

Proxy objects

Proxy objects can be used to intercept object property accesses at runtime. This can be used for meta-programming in many real-world situations, e.g.:

  • preventing, auditing and logging property accesses
  • calculated / derived properties
  • adding a compatibility layer on top of an object

Here’s an example that logs property accesses on the proxied object:

using a Proxy object
1
2
3
4
5
6
7
8
9
10
11
var proxy = Proxy.create({
  get: function (obj, name) {
    console.log("read-accessing property '%s'", name);
  },
  set: function (obj, name) {
    console.log("write-accessing property '%s'", name);
  }
});

proxy.foo = "bar";   /* write-accessing property 'foo' */
proxy.foo;           /* read-accessing property 'foo' */

Proxy objects are not available by default. To enable them in arangod or arangosh, start them with the option --javascript.v8-options="--harmony_proxies".

Iterators and generators

ES6 provides generators and iterators. They can be used individually or in combination.

Let’s start with a simple example of a generator that will generate only two values:

a simple generator that generates two values
1
2
3
4
5
6
7
8
9
function* generate () {
  yield 23;
  yield 42;
}

var generator = generate();
console.log(generator.next());  /* { "value" : 23, "done" : false } */
console.log(generator.next());  /* { "value" : 42, "done" : false } */
console.log(generator.next());  /* { "value" : undefined, "done" : true } */

As can be seen above, the value yielded by the generator function will be returned wrapped into an object with a value and a done attribute automatically.

The general pattern to consume all values from a generator function is to call its next() method until its done value is true:

consuming all values from a generator function
1
2
3
4
5
6
7
8
9
var generator = generate();

while (true) {
  value = generator.next();
  if (value.done) {
    break;
  }
  console.log(value.value);
}

An alternative to that is to use an iterator (note the new of operator):

consuming all values from a generator function
1
2
3
4
5
var generator = generate();

for (var value of generator) {
  console.log(value);
}

Generator functions produce their values lazily. Therefore it is possible and not inefficent to write generators that produce never-ending sequences. Though one must be careful to abort iterating over the generator values at some point if the sequence does not terminate:

a generator producing an endless sequence
1
2
3
4
5
6
7
8
9
10
11
12
13
function* generate () {
  var i = 0;
  while (true) {
    yield ++i;
  }
}

var generator = generate();

/* note: this will not terminate */
for (var value of generator) {
  console.log(value);
}

We have now seen two uses of iterators as part of the previous examples. As demoed, generator function values can be iterated with the of operator without any further ado. Apart from generators, a few other built-in types also provide ready-to-use iterators. The most prominent are String and Array:

iterating over the characters of a string
1
2
3
4
5
var text = "this is a test string";

for (var value of text) {
  console.log(value);
}

The above example will iterate all the individual characters of the string.

The following example iterates the values of an Array:

iterating over the values of an array
1
2
3
4
5
var values = [ "this", "is", "a", "test" ];

for (var value of values) {
  console.log(value);
}

This will produce "this", "is", "a", "test". This is normally what is desired when iterating over the values of an Array. Compare this to the in operator which would produce 0, 1, 2 and 3 instead.

Map and Set objects also implement iterators:

iterating over the contents of a Map
1
2
3
4
5
6
7
8
9
10
11
var m = new Map();

m.set("Sweden", "Europe");
m.set("China", "Asia");
m.set("Bolivia", "South America");
m.set("Australia", "Australia");
m.set("South Africa", "Africa");

for (var value of m) {
  console.log(value);
}

Note that Maps also provide dedicated iterators for just their keys or their values:

iterating over keys and values of a Map
1
2
3
4
5
6
7
for (var country of m.keys()) {
  console.log(country);
}

for (var continent of m.values()) {
  console.log(continent);
}

Rolling an iterator for your own object is also possible by implementing the method Symbol.iterator for it:

creating an iterator for an object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Sentence (text) {
  this.text = text;
}

/* create the actual iterator method */
/* note that the iterator is a generator function here */
Sentence.prototype[Symbol.iterator] = function*() {
  var regex = /\S+/g;
  var text = this.text;
  var match;
  while (match = regex.exec(text)) {
    yield match[0];
  }
};

var sentence = new Sentence("The quick brown fox jumped over the lazy dog");
/* invoke the iterator */
for (var word of sentence) {
  console.log(word);
}

Generators and iterators are not available by default. To enable them in arangod or arangosh, start them with the option --javascript.v8-options="--harmony_generators".

String enhancements

ES6 provides the following convenience string functions:

  • string.startsWith(what)
  • string.endsWith(what)
  • string.includes(what)
  • string.repeat(count)
  • string.normalize(method)
  • string.codePointAt(position)
  • String.fromCodePoint(codePoint)

These functions are mostly self-explaining, so I won’t explain them in more detail here. Apart from that, these functions are turned off by default. To enable them in arangod or arangosh, start them with the option --javascript.v8-options="--harmony_strings".

Array enhancements

ES6 provides the following enhancements for the Array object:

  • array.find(function)
  • array.findIndex(function)
  • array.keys()
  • array.values()
  • Array.observe(what, function)

Here are a few examples demoing these functions:

Array enhancements
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var values = [ 1, 2, 9, 23, 42 ];

/* returns the first Array element for which the function returns true */
values.find(function (value) {
  return value === 23;
});

/* returns the first Array index for which the function returns true */
values.findIndex(function (value) {
  return value === 23;
});

/* iterate over the keys of the Array */
for (var key of values.keys()) {
  console.log(key);
}

/* iterate over the values of the Array */
for (var key of values.values()) {
  console.log(key);
}

/* observe all changes to an Array */
Array.observe(values, function (changes) {
  console.log(changes);
});

/* trigger a change to the observed Array */
values.push(117);

The Array enhancements are turned off by default. To enable them in arangod or arangosh, start them with the option --javascript.v8-options="--harmony_arrays".

Number enhancements

The Number object is extended with the following ES6 functions:

  • Number.isInteger(value)
  • Number.isSafeInteger(value)

There are also Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER so applications can programmatically check whether a numeric value can still be stored in the range of -253 to +253 without potential precision loss.

Constants

The const keyword can be used to define a read-only constant. The constant must be initialized and a variable with the same name should not be redeclared in the same scope.

Here is an example of using const:

using const to create a read-only variable
1
2
3
4
function calculate (value) {
  const magicPrime = 23;
  return magicPrime ^ value;
}

In non-strict mode, const variables behave non-intuitively. Re-assigning a value to a variable declared const does not throw an exception, but the assignment will not be carried out either. Instead, the assignment will silently fail and the const variable will keep its original value:

re-assigning a value to a const variable
1
2
3
4
5
function mystery () {
  const life = 42;
  life = 23;        /* does not change value and does not throw! */
  return life;      /* will return 42 */
}
re-assigning a value to a const variable, using strict mode
1
2
3
4
5
function mystery () {
  "use strict";
  const life = 42;
  life = 23;        /* will throw SyntaxError "assignment to constant variable" */
}

The const keyword is disabled by default. To enable it in arangod or arangosh, start them with the option --javascript.v8-options="--harmony_scoping".

Enhanced object literals

ES6 provides a shorthand for defining methods in object literals.

The following example creates a normal method named save in myObject:

shorthand method declaration
1
2
3
4
5
6
var myObject = {
  type: "myType",
  save () {
    console.log("save");
  }
};

Interestingly, the object literals seem to work for method declarations only. I did not get them to work for non-method object properties, though ES6 allows that. It seems that this is not implemented in V8 yet.

Enhanced object literals are turned off by default. To enable them in arangod or arangosh, start them with the option --javascript.v8-options="--harmony_object_literals".

Enhanced numeric literals

For the ones that love working with binary- or octal-encoded numbers, ES6 has support for this too:

numeric literals
1
2
var life = 0b101010;          /* binary, 42 in decimal */
var filePermissions = 0o777;  /* octal, 511 in decimal */

Enhanced numeric literals are turned off by default. To enable them in arangod or arangosh, start them with the option --javascript.v8-options="--harmony_numeric_literals".

Symbols

ES6 also provides a Symbol type. Symbols are created using the global Symbol() function. Each time this function is called, a new Symbol object will be created. A Symbol can be given an optional name, but this name cannot be used to identify the Symbol later. However, Symbols can be compared by identity.

What one normally wants is to use the same Symbol from different program parts. In this case, a Symbol should not be created with the Symbol() function, but with Symbol.for(). This will register the Symbol in a global symbol table if it is not there yet, and return the Symbol if already created:

using named Symbols
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var typeAttribute = Symbol.for("type");
var carType = Symbol.for("car");
var trainType = Symbol.for("train");

var object1 = { };
object1[typeAttribute] = carType;

var object2 = { };
object2[typeAttribute] = trainType;

/* check if the objects have the same type */
object1[typeAttribute] === object2[typeAttribute];  /* false */
object1[typeAttribute] === carType;                 /* true */
object2[typeAttribute] === carType;                 /* false */
object2[typeAttribute] === trainType;               /* true */

Symbol object properties are not enumerated by default, so they can be used to implement “hidden” or internal properties.

Symbols can be used by default in arangod and arangosh. No special configuration is required.

TypedArrays

TypedArrays are Arrays whose members all have the same type and size. They are more specialized (read: limited but efficient) alternatives to the all-purpose Array type.

TypedArrays look and feel a bit like C arrays, and they are often used as an Array-like view into binary data (for which JavaScript has no native support).

A TypedArray is created (and all of its memory is allocated) by invoking the appropriate TypedArray constructor:

  • Int8Array
  • Uint8Array
  • Uint8ClampedArray
  • Int16Array
  • Uint16Array
  • Int32Array
  • Uint32Array
  • Float32Array
  • Float64Array
using an Array of unsigned 8 bit integers
1
2
3
4
5
6
var data = new Uint8Array(2);
data[0] = 0b00010101;  /* 23 */
data[1] = 0b00101010;  /* 42 */

console.log(data[0]);  /* 23 */
console.log(data.length * data.BYTES_PER_ELEMENT); /* 2 bytes */
using an Array of 64 bit floating point values
1
2
3
4
5
6
var data = new Float64Array(2);
data[0] = 23.23;

console.log(data[0]);  /* 23.23 */
console.log(data[1]);  /* 0.0 */
console.log(data.length * data.BYTES_PER_ELEMENT); /* 16 bytes */

TypedArrays can be used in arangod and arangosh by default. No special configuration is required to activate them.

Unsupported ES6 features

As mentioned before, V8 does not yet support every proposed ES6 feature. For example, the following ES6 features are currently missing:

  • template strings
  • function default parameters
  • rest function parameter
  • spread operator
  • destructuring
  • array comprehension
  • let

I strongly hope these features will make it into the final version of ES6 and will be implemented by the V8 team in future versions of V8.

Apart from that, a lot of nice ES6 features are there already and can be used in ArangoDB applications.