In ActionScript Puzzlers I presented a list of 10 puzzlers meant to test your knowledge of the language. Without starting there, this post isn’t going to make a lot of sense. I also encourage you to try running the puzzler code yourself before coming back here. If it doesn’t do what you expect, try stepping through or modifying the code until you understand what’s going on.
In this post I’ll reveal the answers to each puzzler, and ways to avoid similar pitfalls in your own coding.
Puzzle 1: Looping
Here’s the output of a call to loopy()
:
BEE,BOP,BOP,BOO
Why the extra BOP
? This code demonstrates the lack of block scope in ActionScript. Although it appears that caps
is defined only within the for loop, it is actually defined at the scope of the function. Therefore for the second null
in the array, caps
was not overwritten, but instead retained its value from the previous iteration of the loop.
The ActionScript compiler effectively shifts all the variable declarations to the top of the method before running your code. This explains why the compiler doesn’t complain when you use a variable before declaring it, or why it gives you a warning when you declare the same variable in two separate loops. This can be especially confusing for programmers with a C# or Java background.
The same issue holds true for JavaScript. Yahoo’s JavaScript architect Douglas Crockford advocates declaring all your local variables at the start of your functions. This avoids any ambiguity about the scope of your local variables. This advice holds true for ActionScript as well.
For the somewhat contrived puzzler example, the following rewrite produces just the one BOP
.
private function loopy() : String { var names : Array = [ null, "bee", "bop", null, "boo" ]; var name : String; var result : Array = []; var caps : String; for each (name in names) { caps = null; if (name != null) { caps = name.toUpperCase(); } if (caps != null) { result.push(caps); } } return result.join(','); }
You could further reduce the for loop as follows…
for each (name in names) { if (name != null) { caps = name.toUpperCase(); result.push(caps); } }
Puzzle 2: Sorted
Here’s the output of a call to sorted()
:
[ -22, -5, 1, 1, 22, 33, 4, 71 ] (length=8)
By default, Array.sort()
sorts using Unicode values, so 4 appears after 22 and 33. To search numerically replace line 3 with:
var sorted : Array = values.sort(Array.NUMERIC);
This produces [ -22, -5, 1, 1, 4, 22, 33, 71 ] (length=8)
A cursory look at the API, may make you think values.sort(Array.NUMERIC | Array. UNIQUESORT);
would eliminate the duplicate 1. In actual fact, that call does something far less useful – it returns 0
to indicate the array contains a duplicate.
The new Vector
class has an almost identical list of methods to Array
, except they wisely chose to replace the confusing sort methods with one that simply takes a typed compare function.
Puzzle 3: Delimiters
Here’s the output of a call to delimiters()
:
[ 1, 2, 3 ] (length=3)
[ 1, 2, 3 ] (length=3)
[ 1, 2, 3, ] (length=4)
[ , 1, 2, 3 ] (length=4)
[ , , 1, 2, 3, ] (length=6)
ActionScript is inconsistent in how it handles delimiters in array literals. A trailing comma is ignored. A leading comma, or whitespace followed by a comma within the list results in a undefined
element added to the Array
.
Puzzle 4: Indecisive
Here’s the output of a call to responseOrError(indecisive)
:
finally wins
In fact finally always ‘wins’ with any combination of try, catch or finally blocks throwing exceptions or returning values. It does make for confusing reading though. It’s generally best to only have a single return call in any function and for it to be the last statement. This may feel like a step backwards, but there’s almost always an elegant way of doing this.
Puzzle 5: Illusive dates
Here’s the output of a call to dates()
: start=2009 08 31 finish=2010 03 15
Much like Java, the ActionScript Date
class is nasty.
- day of the month is 1-indexed , but month is 0-indexed
- there’s no static constants for days of the week or months of the year to aid readability
- confusing property names:
date
is day of the month,time
is milliseconds since epoch timezoneOffset
is read-only so it’s impossible to cleanly represent timezones other than the local timezone
At first glance, the start date looks like 31st July, but it’s actually 31st August due to month being 0-indexed. finish
was messed up because we applied the new date incrementally. See the comments added below for the value of date after each statement.
private function dates() : String { var formatter : DateFormatter = new DateFormatter(); formatter.formatString = "YYYY MM DD"; var date : Date = new Date( 2009, 7, 31); // 31st Aug 2009 var result : String = "start=" + formatter.format(date); date.fullYear = 2010; // 31st Aug 2010 date.month = 1; // 3rd Mar 2010 date.date = 15; // 15th Mar 2010 result += " finish=" + formatter.format(date); return result; }
When setting the month, the day of the month is too big to match any valid day in February. It’s over by 3, so the date is rolled ahead by this amount into March. Finally the day of the month is overwritten with 15.
To avoid surprises like this in your code, add constants for the months, and make your date changes atomically.
public static const JANUARY : int = 0; public static const FEBRUARY : int = 1; public static const MARCH : int = 2; public static const APRIL : int = 3; public static const MAY : int = 4; public static const JUNE : int = 5; public static const JULY : int = 6; public static const AUGUST : int = 7; public static const SEPTEMBER : int = 8; public static const OCTOBER : int = 9; public static const NOVEMBER : int = 10; public static const DECEMBER : int = 11; private function dates() : String { var formatter : DateFormatter = new DateFormatter(); formatter.formatString = "YYYY MM DD"; var date : Date = new Date( 2009, AUGUST, 31); var result : String = "start=" + formatter.format(date); date.setFullYear(2010, FEBRUARY, 15); result += " finish=" + formatter.format(date); return result; }
The updated function call now returns: start=2009 08 31 finish=2010 02 15
Puzzle 6: Sparsity
Here’s the output of a call to sparse()
:
Array: length=101 count=3 Vector: length=101 count=101 ArrayCollection: length=101 count=101
Arrays
can be sparse, as in this example, where setting a value at a high index does not fill in all the values in between. Effectively Array
is just like a normal Object
, except:
a) it has a bunch of handy collection orientated methods you have at your disposal
b) it supports the very handy array literal format
Vectors
, in contrast, are typed, are not sparse, and do not support the addition of properties with names other than positive integers (this is enforced at runtime, not compiler time). These limitations make live much easier for the VM. Lookups should now be constant time operations.
Even though ArrayCollection
wraps an Array
, it does not retain the appearance of being sparse. When you loop through the collection, it ‘fills in’ the gaps with null
values.
Puzzle 7: Mixed up
If you ran the following code, you would find that mixedUp()
did not return anything at all, but instead throws the following error:
TypeError: Error #1010: A term is undefined and has no properties.
What’s gone wrong? Despite first appearances, the objectLiteral()
function does not in fact return an Object
, but instead returns undefined
. This is due to ActionScript repeating the mistake made in JavaScript; making semicolons optional at the end of statements. Here the compiler interpreted the code as an empty return statement, following by a block containing two labels. I was actually a little bit sneaky here, as that is not even a valid object literal, since I missed out the comma.
To avoid this happening in your code, whenever splitting a statement over multiple lines always finish any line with a symbol that couldn’t possibly be the end of a statement. For instance: || && , + - * / .
You should also consider using the C/Java style syntax for blocks having the { at the end of the previous line, rather than on a line on its own. I’ve used this style in all my examples.
Here’s the code after fixing the objectLiteral()
function:
private function objectLiteral() : * { return { a : 123, b : "888" }; } private function mixedUp() : String { var lit : * = objectLiteral(); return lit.a + lit.b; }
This amended code now outputs the following:
123888
If you’re wondering why it doesn’t output 1011, the + operator uses string concatenation if either argument is a String
.
Puzzle 8: Unicoder
Clued up readers will recognize this example from the Java Puzzlers book (Puzzle 14). The reason I included it, is that it’s one of the examples that produces quite different results in ActionScript.
\u0022 is unicode for "
(double quote). In Java, Unicode characters are given first class treatment, with the compiler seeing it as no different from an actual double quote. In this case the statement becomes the equivalent of:
return "a".length + "b".length; // returns 2
Here’s the output of a call to escaped()
in ActionScript:
14
ActionScript recognizes the characters as Unicode, but it doesn’t translate them into symbols until after the parser has translated the source into tokens. It treats the Unicode as if it were escaped characters. The following code produces identical results.
private function escaped() : int { return "a\".length + \"b".length; }
Puzzle 9: Constantly confused
Create the following two classes:
package { public class A { protected const FOO : String = "foo"; } }
package { public class B extends A { public var result : String; public function B() { result = 'before: ' + FOO; super(); result += ' after: ' + FOO; } } }
Then try running the following:
private function superDuper() : String { return new B().result; }
Calling superDuper()
will return the following result:
before: null after: foo
Unlike Java, ActionScript lets you position a call to super()
anywhere you like in your constructor. If you don’t include a call to super()
yourself, the compiler will add one at the start of your constructor. This flexibility comes with extra responsibility. Constructor code before super()
must not rely on member variables or constants in any superclass. In this case, changing the FOO
constant to be static will result in it always being properly initialized.
Puzzle 10: Extended Array
If you attempted to run arrayMax()
any number of things could happen.
Under Flex 3.4 I get the following result:
len=3 max=NaN
Under Flex 4.0, I generally get the same as Flex 3.4, but if I chose to display the results in a Spark TextArea, I get the following error:
TypeError: Error #1034: Type Coercion failed: cannot convert Function-9 to flashx.textLayout.container.ContainerController.
What is going wrong here? Our fancy new max()
function is now being returned when iterating through any Array! Even though the length of numbers
is correctly reported as 3, if you were to iterate through the items using for each
, you would find there are in fact 4 values.
To prevent this from happening, you need to indicate that this function should not be included in iterators, using the setPropertyIsEnumerable
method.
Here’s an updated version of arrayMax()
that now produces the expected result: len=3 max=10
private function arrayMax() : String { Array.prototype.max = function() : Number { var array : Array = this; var result : Number = Number.MIN_VALUE; for each ( var item : Number in array ) { result = Math.max( result, item ); } return result; }; Array.prototype.setPropertyIsEnumerable("max", false ); var numbers : Array = [ 1, 2, 10]; return StringUtil.substitute('len={0} max={1}', numbers.length, numbers.max()); }