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.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 | |
---|---|
1 | a |
struct | |
---|---|
1 | b |
struct | |
---|---|
1 | c |
struct | |
---|---|
1 | d |
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 | |
---|---|
I | 1 |
V | a |
struct | |
---|---|
I | 2 |
V | b |
struct | |
---|---|
I | 3 |
V | c |
struct | |
---|---|
I | 4 |
V | d |
(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 |
| ||||
2 |
|
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 |
| ||||
2 |
| ||||
3 |
|
array | |||||
---|---|---|---|---|---|
1 |
| ||||
2 |
| ||||
3 |
|
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 |
| ||||||||||||||||
2 |
|
array | |||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 |
| ||||||||||||||||
2 |
|
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 | |
---|---|
1 | 1 |
2 | [undefined array element] Element 2 is undefined in a Java object of type class coldfusion.runtime.Array. |
3 | 3 |
array | |
---|---|
1 | 1 |
2 | [undefined array element] Element 2 is undefined in a Java object of type class coldfusion.runtime.Array. |
3 | 9 |
array | |
---|---|
1 | 1 |
2 | 0 |
3 | 9 |
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 |
| ||||||||||||||||||||||||||
2 |
|
array | |||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 |
| ||||||||||||||||||||||||||
2 |
|
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:arrayReduce()
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
- Whero
- Karaka
- Kowhai
- Kakariki
- Kikorangi
- Tawatawa
- Mawhero
<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
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