Tuesday, 25 February 2014

ColdFusion 11: .map() and .reduce()

G'day:
More ColdFusion 11 testing. This time I look at the new .map() and .reduce() methods that each of array, struct and lists now have. It's mostly good news.

ColdFusion has increased its repertoire of object-iterator functions further in ColdFusion 11. In ColdFusion 10 it had the following:
listEach(), for some reason was not implemented in ColdFusion 10, but is there in ColdFusion 11.

Recap

Just to recap on the each() and filter() functionality, here's a quick example of the array versions of each of them:

each()


letters = ["a","b","c","d"];
arrayEach(letters, function(){
    writeDump(arguments);
});

On ColdFusion 10, we get this:

struct
1a
struct
1b
struct
1c
struct
1d

So we see that arrayEach() does what it says on the tin: it calls the callback for each element of the array. And the callback receives the value of each array element. Now on ColdFusion 11, this code yields... bugger all. It doesn't error, but it doesn't do anything. This had me scratching my head for quite a while, but I just cracked it... On ColdFusion 11 one must specify the arguments of the callback (this should not be necessary), thus:

arrayEach(letters, function(v,i){
    writeDump(arguments);
});

Then it works:

struct
I1
Va
struct
I2
Vb
struct
I3
Vc
struct
I4
Vd

(One doesn't nee to specify both arguments; just the index one is fine. This is a bug, and I will raise it accordingly: 3713035. listEach() has the same problem: one needs to specify the argument in the callback definition, or the function doesn't work).

Anyway, it's a good enhancement to ColdFusion 11 that the callback also receives the index as well the value.

filter()

The filter methods also iterate over the given object and returns a new object. The callback returns a boolean which determines whether the current element is returned in the new object, eg:

numbers = "1,2,3,4";
odds = listFilter(numbers, function(v){
    return v MOD 2;
});    
writeDump([{numbers=numbers},{odds=odds}]);

Here the callback returns true for each odd list element, so we end up with a list with just the odd numbers in it:

array
1
struct
NUMBERS1,2,3,4
2
struct
ODDS1,3

So those are the ones from ColdFusion 10. Old news.


map()

The map() functions iterate over the collection (be it a list, array or struct), and returns a new object with an element for each of the ones in the original collection. The callback in this case returns the new element for the new collection, which is derived from the original collection. So it remaps the original collection. Here's examples of each of them:

listMap()

This function doesn't work, I'm afraid (or I'm doing something wrong which I cannot identify). We're not off to a good start here. Here's some sample code:

rainbow    = "Whero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero";

externalList = "";
reverseRainbow = listMap(rainbow,function(v,i,l){
    var newValue = "#i#:#v.reverse()#";
    externalList = externalList.append(newValue);
    return newValue;
});
writeDump([{rainbow=rainbow},{reverseRainbow=reverseRainbow},{externalList=externalList}]);

externalList = "";
reverseRainbow = rainbow.map(function(v,i,l){
    var newValue = "#i#:#v.reverse()#";
    externalList = externalList.append(newValue);
    return newValue;
});
writeDump([{rainbow=rainbow},{reverseRainbow=reverseRainbow},{externalList=externalList}]);

This contains the same example using both the listMap() function, and the .map() method. Here's the output:

array
1
struct
RAINBOWWhero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero
2
struct
REVERSERAINBOWWhero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero
3
struct
EXTERNALLIST1:orehW,2:akaraK,3:iahwoK,4:ikirakaK,5:ignarokiK,6:awatawaT,7:orehwaM
array
1
struct
RAINBOWWhero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero
2
struct
REVERSERAINBOWWhero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero
3
struct
EXTERNALLIST1:orehW,2:akaraK,3:iahwoK,4:ikirakaK,5:ignarokiK,6:awatawaT,7:orehwaM

I've added in the externalList to show you what reverseRainbow should look like. listMap() is supposed to work that the callback receives the element value, its index, and the whole list as arguments. Then it uses that information however is appropriate to create and return a new element. Here the new element is very contrived: the element index, and its value (reversed).

However the original list is just being returned by listMap() here. That's wrong. Bug: 3713038.

Also note listMap() takes some other arguments I'm not using here: the list delimiter and a flag as to whether to respect empty list items (this is like most/all other list functions). But there's another slight glitch here: those values should also be passed to the callback, as they might be necessary for doing the element remapping. Bug: 3713043.

It looks like we're off to a shocking start here, but that's the end of the bugs I found.

arrayMap()

In this example we can get a better idea of how the mapping process is supposed to work. Here's an example using both the function and the method:

rainbow    = ["Whero","Karaka","Kowhai","Kakariki","Kikorangi","Tawatawa","Mawhero"];
colourInList = arrayMap(
    rainbow,
        function(v,i,a){
        return replace(a.toList(), v, ucase(v));
    }
);
writeDump([rainbow,colourInList]);


rainbow.map(function(v,i,a){
    return replace(a.toList(), v, ucase(v));
});
writeDump([rainbow,colourInList]);

array
1
array
1Whero
2Karaka
3Kowhai
4Kakariki
5Kikorangi
6Tawatawa
7Mawhero
2
array
1WHERO,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero
2Whero,KARAKA,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero
3Whero,Karaka,KOWHAI,Kakariki,Kikorangi,Tawatawa,Mawhero
4Whero,Karaka,Kowhai,KAKARIKI,Kikorangi,Tawatawa,Mawhero
5Whero,Karaka,Kowhai,Kakariki,KIKORANGI,Tawatawa,Mawhero
6Whero,Karaka,Kowhai,Kakariki,Kikorangi,TAWATAWA,Mawhero
7Whero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,MAWHERO
array
1
array
1Whero
2Karaka
3Kowhai
4Kakariki
5Kikorangi
6Tawatawa
7Mawhero
2
array
1WHERO,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero
2Whero,KARAKA,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero
3Whero,Karaka,KOWHAI,Kakariki,Kikorangi,Tawatawa,Mawhero
4Whero,Karaka,Kowhai,KAKARIKI,Kikorangi,Tawatawa,Mawhero
5Whero,Karaka,Kowhai,Kakariki,KIKORANGI,Tawatawa,Mawhero
6Whero,Karaka,Kowhai,Kakariki,Kikorangi,TAWATAWA,Mawhero
7Whero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,MAWHERO

Here the callback receives the array element value, its index, and the entire array. From this, we convert the array to a list, and then highlight (with capitals) the current element in that list, returning the whole list. So the resultant remapped array contains an element for each original element, but completely different data than the original array; with each new element being that remapping done in the callback. And the method version works exactly the same (I'm both demonstrating and testing here too, hence the double-up).

If you have a sparse array - one without an element at each index - you need to deal with this by hand. The iteration is index-centric, not element-centric, so each index will have the callback called on it, so you need to deal with the possibility of no value being passed to the callback:

a = [1];
a[3] = 3;
writeDump(a);

result = a.map(function(v,i,a){
    if (structKeyExists(arguments, "v")){
        return v^2;
    }
});
writeDump(result);

result = a.map(function(v=0,i,a){
    return v^2;
});
writeDump(result);

Output:

array
11
2[undefined array element] Element 2 is undefined in a Java object of type class coldfusion.runtime.Array.
33
array
11
2[undefined array element] Element 2 is undefined in a Java object of type class coldfusion.runtime.Array.
39
array
11
20
39

Here I am using two different techniques. In the first version I am simply checking to see if the value exists, and only returning a mapped value if so. In the second version I am defaulting the value in the callback definition. It's really situation-dependent as to which approach to take. In this case, the second approach is not really appropriate.

structMap()

This is more of the same really. Here the callback receives the key and the value:

original = {"one"={1="tahi"},"two"={2="rua"},"three"={3="toru"},"four"={4="wha"}};
fixed = structMap(original, function(k,v){
    return v[v.keyList().first()];
});
writeDump([original,fixed]);

fixed = original.map(function(k,v){
    return v.keyList().first();
});
writeDump([original,fixed]);

array
1
struct
four
struct
4wha
one
struct
1tahi
three
struct
3toru
two
struct
2rua
2
struct
fourwha
onetahi
threetoru
tworua
array
1
struct
four
struct
4wha
one
struct
1tahi
three
struct
3toru
two
struct
2rua
2
struct
four4
one1
three3
two2

Here the value being passed into the callback is the substruct with the digit as a key and the Maori number for a value. I am using that information to map to a struct which has just the Maori version as the new struct's value (the structMap() example); and in the .map() example I'm just mapping to a struct with the digit as values. These are pretty contrived examples, but you hopefully get the idea.

.reduce()

The reduce() operation is slightly more complex. Basically it iterates over the collection and from each element of the collection, derives one single value as a result.

listReduce()

Here we perform a sum and a product on a list of digits:

numbers = "1,2,3,4,5,6,7,8,9,10";

sum = listReduce(
    numbers,
    function(previousValue, value){
        return previousValue + value;
    },
    0
);
writeOutput("The sum of the digits #numbers# is #sum#<br>");

product = numbers.reduce(
    function(previousValue, value){
        return previousValue  * value;
    },
    1
);
writeOutput("The product of the digits #numbers# is #product#<br>");

Note that the callback receives two arguments: the previous value, and the current value. And also note the function can accept a starting value too. That said, it doesn't need to take a starting value, but if you don't give it one, you have to deal with not receiving a value for it in the first iteration. One can handle this like this:

numbers = "1,2,3,4,5,6,7,8,9,10";

sum = numbers.reduce(function(previousValue, value){
    if (!structKeyExists(arguments, "previousValue")){
        return value;
    }
    return previousValue + value;
});
writeOutput("The sum of the digits #numbers# is #sum#<br>");

Or like this:

product = numbers.reduce(function(previousValue=1, value){
    return previousValue * value;
});
writeOutput("The product of the digits #numbers# is #product#<br>");

To be honest, given one can default the previousValue argument in the callback definition, I wonder if the listReduce() function itself needs to have that initialValue argument? It seems like pointless duplication to me, perhaps? Hopefully someone who has more experience with these functions (Adam Tuttle and Sean, I am looking at you two), and can advise where one or other approach might be better.

Oh... and the output from all this (for the sake of completeness):

The sum of the digits 1,2,3,4,5,6,7,8,9,10 is 55
The product of the digits 1,2,3,4,5,6,7,8,9,10 is 3628800


arrayReduce()

This works the same way:

rainbow = ["Whero","Karaka","Kowhai","Kakariki","Kikorangi","Tawatawa","Mawhero"];;

ul = arrayReduce(
    rainbow,
    function(previousValue, value){
        return previousValue & "<li>#value#</li>";
    },
    "<ul>"
) & "</ul>";
writeOutput(ul);

ol = rainbow.reduce(
    function(previousValue, value){
        return previousValue & "<li>#value#</li>";
    },
    "<ol>"
) & "</ol>";
writeOutput(ol);

Actually this is probably a good demonstration of the subtle difference between using a starting value and defaulting the previous value. <ul> makes a sensible starting value, perhaps; but does not make sense as a default previous value.

And the output this time is just some mark-up:

  • Whero
  • Karaka
  • Kowhai
  • Kakariki
  • Kikorangi
  • Tawatawa
  • Mawhero
  1. Whero
  2. Karaka
  3. Kowhai
  4. Kakariki
  5. Kikorangi
  6. Tawatawa
  7. Mawhero
(and, yes, I know I could have just generated the <li> tags with the reduction, then slapped the <ul> and <ol> around them afterwards, but that's not the point ;-)

structReduce()

This example is very similar to the previous one:

rainbow = {
    "Red"="Whero",
    "Orange"="Karaka",
    "Yellow"="Kowhai",
    "Green"="Kakariki",
    "Blue"="Kikorangi",
    "Indigo"="Tawatawa",
    "Pink"="Mawhero"
};

dl = structReduce(
    rainbow,
    function(previousValue, key, value){
        return previousValue & "<dt>#key#</dt><dd>#value#</dd>";
    },
    "<dl>"
) & "</dl>";
writeOutput(dl);

dl = rainbow.reduce(
    function(previousValue, key, value){
        return previousValue & "<dt>#value#</dt><dd>#key#</dd>";
    },
    "<dl>"
) & "</dl>";
writeOutput(dl);

Output:
Blue
Kikorangi
Yellow
Kowhai
Green
Kakariki
Pink
Mawhero
Indigo
Tawatawa
Orange
Karaka
Red
Whero
Kikorangi
Blue
Kowhai
Yellow
Kakariki
Green
Mawhero
Pink
Tawatawa
Indigo
Karaka
Orange
Whero
Red
Here I just demonstrate how I'm using both the key and value from the struct.

That's about it.

Do you know what I am left wondering though... where are the equivalent methods for query objects? They're collections too, after all. I better get a ticket in to get those provided for too: 3713323.

Bed time for me. It's been a very bloody long ColdFusion 11 day for me. I'm scooting back to the UK tomorrow... 30-odd hours worth of aircraft and airports. Joy. At least once I get back I then have 1.5h on the train too. This will quite possibly mark the end of the torrent of ColdFusion 11 stuff from me. I've done my best to blog as much as I can whilst I've been off work, but after today I'll be indisposed, in Ireland, or back at work next Monday. So I'll probably drop back to around one article per day, tops.

--
Adam