fredag den 7. august 2009

Higher-order array methods

A most welcome addition in ES5 is the new higher-order array-methods: map, filter, reduce, some, every, forEach, reduce and reduceRight. They have long been available in various JavaScript frameworks, but it is nice to have them as part of the core language. In relation to Mascara the really cool thing is how they integrate with the type system - but I'll get back to that.

A higher-order function (or method) is defined as a function which takes another function as one of the arguments. This is especially useful when working with arrays, since the supplied function can be executed on some or all items in the array.

map

map applies a function to every item in an array, and returns a new array with the results. Example:

var numbers = [1,2,3,4]
function double(x) {
return x * 2;
}
numbers.map(double) --returns--> [2,4,6,8]

If you only use the supplied function in this context, you can also provide an anonymous function inline:

numbers.map(function(x) { return x * 2; })

In these cases the new shorthand-function syntax comes in handy:

If a function body is a single expression, the brackets and "return"-statement can be left out, like:

numbers.map(function(x) x * 2)

This shorthand syntax is also known as a "expression closure" or "lambda".

filter

Filter filters a list by only returning the items for which a condition is true. The condition is checked by supplying a function which returns true or false for a given item:

numbers.filter(function(x) x>2) --> [3,4]

While map and filter are quite useful tool, I want to point out that array comprehensions are a very cool "syntactic sugar" for mapping and filtering. This filter/map method chaining:

var x = [1,2,3,4,5].filter(function(x)x<7).map(function(x)x*2)

is equivalent to this array comprehension:

var x = [x*2 for each (x in [1,2,3,4,5]) if (x<7)]

Personally I find the array comprehension syntax more elegant, although that may be a matter of taste. One technical advantage of array comprehensions as implemented in Mascara is that they support not just Array-instances, but also collections like HTML node lists:

[e.value
for each (e in document.getElementsByTagName("INPUT"))
if (e.type=="text")]

But I digress.

some and every

some and every are related to filter since they also take functions that returns a boolean for an item.

some returns true if the condition is true for at least one item in the list, while every return true if the condition is true for all items.

Example:

numbers.some(function(x) x>2) --> true (because some of the values are greater than 2)

numbers.every(function(x) x>2) --> false (because not all values are greater than 2)

forEach simply executes a function for each item in the list. It doesn’t return anything, so this is done for side-effects. E.g.

x.forEach(function(x) { alert(x); });

Note that I am using a real function body here, not a shortcut. Since the function passed to forEach should not return anything, it is not appropriate to use an expression.

The forEach method is pretty similar to using the for-each loop:

for each (x in numbers) { alert(x); }

Lastly we have reduce and its mirror-twin reduceRight. They are a bit tricky to explain. reduce takes a function and an initial value as arguments. It executes the provided function with the initial value and the first item as arguments. The result of this is used as input to execute the function on the next item, and so on. The result of executing the function on the last item is the result of the reduce.

For example, the product of all items in a list can be calculated like this:

x.reduce(function(product, item) product * item, 1) ----> 24

And the sum:

x.reduce(function(sum : int, item : int) sum + item, 0) ----> 10

(Aside: In the last example I provide type annotations to indicate that the parameters are integers. This is because + works on both numbers and strings, so I help the compiler by indicating that they will be numbers (and hence the result will always be numeric. This is not necessary with *, since * only works on numbers anyway)

reduceRight is the same except it traverses the list backwards, i.e. it first executes the function on the last item, then the next-to last and so on.

To be honest I think reduce and reduceRight are somewhat confusing - especially with more complex operations than just summing. The same thing can always be achieved with an ordinary for-each loop, and - to me at least - in a more obvious way:

var sum = 0;
for each (var item in x) sum += x;

However, your mileage may vary. If you have a background in functional programming, the reduces may seem more natural, since you avoid a mutable variable.

The library that implements the methods is only included in the generated code if some these methods are actually used. Generally Mascara attempt to create as lean code as possible, hence boilerplate is only included when it is necessary.

Ingen kommentarer:

This blog has moved to http://blog.mascaraengine.com. See you!

This blog is (was) for news, announcements and questions regarding the
Mascara ECMAScript 6 -> JavaScript translator.