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:
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:
(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:
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 re
maps the original collection. Here's examples of each of them:
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 |
RAINBOW | Whero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero |
|
2 |
struct |
REVERSERAINBOW | Whero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero |
|
3 |
struct |
EXTERNALLIST | 1:orehW,2:akaraK,3:iahwoK,4:ikirakaK,5:ignarokiK,6:awatawaT,7:orehwaM |
|
array |
1 |
struct |
RAINBOW | Whero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero |
|
2 |
struct |
REVERSERAINBOW | Whero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero |
|
3 |
struct |
EXTERNALLIST | 1: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.
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 |
1 | Whero |
2 | Karaka |
3 | Kowhai |
4 | Kakariki |
5 | Kikorangi |
6 | Tawatawa |
7 | Mawhero |
|
2 |
array |
1 | WHERO,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero |
2 | Whero,KARAKA,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero |
3 | Whero,Karaka,KOWHAI,Kakariki,Kikorangi,Tawatawa,Mawhero |
4 | Whero,Karaka,Kowhai,KAKARIKI,Kikorangi,Tawatawa,Mawhero |
5 | Whero,Karaka,Kowhai,Kakariki,KIKORANGI,Tawatawa,Mawhero |
6 | Whero,Karaka,Kowhai,Kakariki,Kikorangi,TAWATAWA,Mawhero |
7 | Whero,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,MAWHERO |
|
array |
1 |
array |
1 | Whero |
2 | Karaka |
3 | Kowhai |
4 | Kakariki |
5 | Kikorangi |
6 | Tawatawa |
7 | Mawhero |
|
2 |
array |
1 | WHERO,Karaka,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero |
2 | Whero,KARAKA,Kowhai,Kakariki,Kikorangi,Tawatawa,Mawhero |
3 | Whero,Karaka,KOWHAI,Kakariki,Kikorangi,Tawatawa,Mawhero |
4 | Whero,Karaka,Kowhai,KAKARIKI,Kikorangi,Tawatawa,Mawhero |
5 | Whero,Karaka,Kowhai,Kakariki,KIKORANGI,Tawatawa,Mawhero |
6 | Whero,Karaka,Kowhai,Kakariki,Kikorangi,TAWATAWA,Mawhero |
7 | Whero,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 |
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 |
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.
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 |
|
one |
|
three |
|
two |
|
|
2 |
struct |
four | wha |
one | tahi |
three | toru |
two | rua |
|
array |
1 |
struct |
four |
|
one |
|
three |
|
two |
|
|
2 |
struct |
four | 4 |
one | 1 |
three | 3 |
two | 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.
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
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
- Whero
- Karaka
- Kowhai
- Kakariki
- Kikorangi
- Tawatawa
- 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 ;-)
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