Saturday 5 September 2015

Expectation management: mapping a changing array

G'day:
I've been staring at this code (/variations thereof) for a week or so now (since JavaScript: running Jasmine unit tests from the CLI; more specifically my JavaScript version of the code I rewrote from "Some CFML code that doesn't work"). I noticed an idiosyncrasy in the first JavaScript version of that code which didn't make sense to me at first. And I ass-u-me`d that my previous expectations made sense and JavaScript was being weird. Then I ran the equivalent code in some other languages and now... not so sure. So here we are: I'm writing a blog article about code I'm not sure I'm completely comfortable with.

It does not help that I'm on my eighth pint of Guinness for the afternoon. But perhaps only as far as my typing goes (which is proving to be a real challenge).

Here's the general gist of what I was trying to do with CFML:

// closure.cfm

letters = ["a","b","c","d","e"];
remappedLetters = letters.map(function(number,index){
    var localCopyOfTheseLetters = duplicate(letters);
    letters.deleteAt(1);
    return localCopyOfTheseLetters;
});
remappedLetters.each(function(series){
    writeOutput(series.toList(" ") & "<br>");
});

Don't worry so much about running that: it does not do what I want (mostly), but more what I'm trying to do, and my expectations of the results.

What I want from this code is to iterate over the letters array, and for each element of it return the whole array from that point. So I'm not really using the callback's value, I'm just leveraging the fact that the map() iteration method loops over each element of the array, so I get to defined - element by element - its replacement.

On Lucee - which is what I tested this with - I get what I want:

a b c d e
b c d e
c d e
d e
e


And this is what I mentally leveraged when working out my code for that "Some CFML code that doesn't work" article. The rest of my logic was predicated on that approach working.

When I tried the same logic on JavaScript:

// closure.js

var letters = ["a","b","c","d","e"];
var remappedLetters = letters.map(function(number,index){
    var localCopyOfTheseLetters = letters.slice();
    letters.shift();
    return localCopyOfTheseLetters;
});
remappedLetters.forEach(function(series){
    console.log(series.join(" "));
});

I get a different result:

C:\src\otherLanguages\js\arrays\map\changeOriginal\js>node closure.js
a b c d e
b c d e
c d e

Huh? Why does it stop after three element? I'm iterating over a five-element array after all. I surmise that under the hood JavaScript is using some sort of Iterator.next() operation. Given I am changing the array I'm iterating over via closure, by the time I get to the third iteration of my map() call, I've lopped two elements off the array, so its length is three, so the map() process exits when Iterator.next() rechecks where it is in the array, and finds out it's at the end. This is entirely supposition, but it seems reasonable.

But initially I was thinking "dumb-arse JavaScript, if I call a method on an object, then the method should be called on the object's current state!". This makes sense for a one-off method, but does it make sense for an iterative method? Hmmm. I had not thought about that. And I had no answer, and given my two comparative cases (Lucee, JavaScript) behaved differently, I didn't know what ought to be the "right" answer. And by "right" I mean "industry standard". I erred towards JavaScript being right, and Lucee being wrong.

Running this example on ColdFusion behaves like JavaScript. But to be honest: I back Adobe to get things right even less than I do LAS, so I filed that as "nice to know".

I did my usual thing of testing out my limited repertoire of other languages running equivalent code.



Ruby?

# closure.rb

letters = ["a","b","c","d","e"]
remappedLetters = letters.map do |number|
    localCopyOfTheseLetters = letters.clone
    letters.shift
    localCopyOfTheseLetters
end

remappedLetters.each do |series|
    puts series.join " "
end

Ruby agrees with ColdFusion and JavaScript:

C:\src\otherLanguages\js\arrays\map\changeOriginal\ruby>ruby closure.rb
a b c d e
b c d e
c d e

Python?

# common.py

letters = ["a","b","c","d","e"]

def mapper(number):
    global letters
    localCopyOfTheseLetters = letters[:]
    letters.pop(0)
    return localCopyOfTheseLetters

# map.py

from common import letters, mapper

remappedLetters = map(mapper, letters)

for series in remappedLetters:
    print(" ".join(series))

Again, agreed with ColdFusion, JavaScript and Ruby:

C:\src\otherLanguages\js\arrays\map\changeOriginal\python>python map.py
a b c d e
b c d e
c d e

I did not forget PHP this time:

// closure.php

$letters = ["a","b","c","d","e"];
$remappedLetters = array_map(function($number) use (&$letters){
    $localCopyOfTheseLetters = $letters;
    array_shift($letters);
    return $localCopyOfTheseLetters;
}, $letters);

foreach ($remappedLetters as $series) {
    echo join($series, " ") . "\n";
}

Here the results matched Lucee's:

C:\src\otherLanguages\js\arrays\map\changeOriginal\php>c:\apps\php\7\php.exe closure.php
a b c d e
b c d e
c d e
d e
e

But... sorry PHP you are noted for doing things stupidly, so I'm not gonna take your result too seriously. Convenient though they are.

So, anyhow, Groovy gave some different and quite pleasing results:

// example.groovy

letters = ["a","b","c","d","e"];
remappedLetters = letters.collect([]){
    localCopyOfTheseLetters = letters
    letters.leftShift()
    return localCopyOfTheseLetters
}

remappedLetters.each() {
    println(it)
}

Results:

C:\src\otherLanguages\js\arrays\map\changeOriginal\groovy>groovy example.groovy
Caught: java.util.ConcurrentModificationException
java.util.ConcurrentModificationException
        at example.run(example.groovy:4)

Now that I know the question has been raised about my (so-called) logic here, I like the way Groovy goes "you what? Nah, you can't do that". This is better than unpredictable results. This is working on the basis that the results of this algorithm I'm running is unclear, which I think I borne out by different languages doing different things.

I was gonna try to do a Clojure example too, knowing it's all about immutable data, to see what it would do. But I also want to get some TDD going with Clojure before I start anything, and I couldn't be arsed doing this this afternoon at the pub, so I've leave it to my readers (at least three of which I know do Clojure) to provide the answer for this.

So I guess my code I stated "did work except for the language wasn't up-to-speed with what I wanted" is a bit... erm... wrong. My expectations here were off, and as ColdFusion (not Lucee) is my baseline for my CFML experimentation, I have to disqualify Lucee, which has a bug here, unfortunately.

What do you think about all this? I fully accept that I've been pretty drunk throughout writing all this (I'm on my ninth pint now), so I dunno how insightful I've been). I also think I might do a quiz to see how people handle fulfilling my original requirement.

--
Adam