Summary
Not only are basic collections (arrays and associative arrays) tightly integrated into the core language, but arrays provide some sophisticated functional programming support. This article also introduces the creation and use of shared libraries and the basics of prototypes.
JavaScript, from which ActionScript is derived, is a bit of a mongrel language and occasionally you'll encounter oddities, which I'll try to point out. However, it seems inevitable that all languages accumulate oddities of one kind or another, and few languages actually remove oddities -- Python 3.0 and an upcoming version of Ruby are the only examples I know of.
Despite the occasional funkiness, the power of the ActionScript language tools along with the hybrid type checking (mostly static for tool support in Flex Builder, but dynamic whenever it's convenient) makes for a programming experience that I find much more straightforward and pleasing than Java.
While much of what I describe here also applies to JavaScript, I have only tested it with ActionScript. JavaScript language features can be different from one browser to the next and also vary with browser versions.
As an aside, I was not a fan of JavaScript after discovering the language syntax and basic features could vary across browsers. Compensating for arbitrary platform differences is not how I want to spend my time. And even though AJAX library creators put a lot of time and effort into writing cross-browser code, you still seem to spend some portion of your time dealing with cross-browser problems, which seems a bad use of brain cycles. ActionScript, however, is learn-once, use-everywhere -- you don't have to worry about whether particular language syntax will run on one browser or another.
In my earlier article on components, I didn't use libraries. But library components are the best way to share code between projects, so the examples in this article will utilize reusable library components.
I've found the coverage on the creation of libraries in books and documentation to be spotty and scattered, so it's important to describe it thoroughly before we get into collections.
If you're a Java programmer, aspects of the library-creation process will be familiar. Each component lives in a namespace, and we want namespaces to be unique (to prevent collisions), so Flex follows the Java practice of beginning the namespace with the reversed URL of the individual or company creating the library. So in my case, my namespace will begin with com.mindviewinc.
You can have multiple components within one namespace, so it makes sense to group them according to functionality. In this article I create three different types of components which I classify as display, format, and functional, so their full namespaces will be com.mindviewinc.display, com.mindviewinc.format, and com.mindviewinc.functional.
Also like Java, the library directory structure must correspond to the namespace, so library components within the com.mindviewinc.display namespace must be placed in the directory com/mindviewinc/display. Mercifully, however, Java's classpath has not been inflicted upon ActionScript programmers; on the contrary, Flex Builder tends to be helpful when you are including libraries. Also, a Flex project typically compiles to a single SWF so installation is a matter of copying the SWF (usually onto your web site).
The final similarity to Java is in the library's component files: each file can have only one public element (class or function). That way, each file only exposes that one element. Note that I didn't find this out through reading; Flex Builder told me when I tried to put more than one public element in a single file.
In Flex Builder, select File | New | Flex Library Project. It doesn't matter what you name your library (that is, you're not constrained by any of the aforementioned rules). I just called mine "Mindview." Right-click (or your machine's equivalent) on the "Mindview" folder, create a directory called "com," and beneath that create "mindviewinc," and beneath that create "display," "format," and "functional." The full directory structure for the library (including the bin directory added by Flex Builder, and things we will add later) looks like this:
Mindview
+ bin
+ com
+ mindviewinc
+ display
+ format
+ functional
+ test
+ includes
Let's create a first library component; although it's simple it turns out to be useful in this article. It is a small modification to the TextArea and it simplifies the display of information.
To create it, right-click on the display folder and select New | ActionScript Class. Call the class TextDisplay. Note that you're not given the option of naming the file; the file name is the same as the class name. You can also tell it what base class to inherit from. Flex Builder knows what package name to use because of the folder you're placing it in.
Here's what the component looks like after the rest of the code has been filled in:
package com.mindviewinc.display {
import mx.controls.TextArea
public class TextDisplay extends TextArea {
public function TextDisplay() {
super()
percentHeight = 100
percentWidth = 100
}
public function show(s:* = "", delim:String = "\n"):void {
text += String(s) + delim
}
}
}
Like Python, Ruby and Groovy, semicolons at the end of lines are optional. The compiler treats the end of a line as the end of a statement unless it's inside a parenthesized expression. The only time you are required to use a semicolon is when you have more than one statement on a single line, or an empty expression.
If you've seen JavaScript or ActionScript code before, you'll note that typical code often makes heavy use of the this keyword. It turns out this use is redundant; like Java and C++, this is only necessary in special cases and the compiler will automatically scope to this. As I am a minimalist when it comes to code syntax, I will tend to leave off both redundant semicolons and redundant usages of this.
The constructor sets the component to full width and height, and the method simplifies the act of displaying text. Note that ActionScript supports default function arguments, which I find very convenient.
Note that show() has an asterisk for the argument type; this means "any type." You actually get any type if you don't specify a type, but then the compiler gives you a warning. The asterisk for the type tells the compiler that you intend this to be of any type. This way, show() can accept anything, then cast it to a String for printing.
For testing, create a little Flex application that uses the component. Select File | New | Flex Project, give it a name and press the "next" button. Press "next" again and press the "Library path" button and then "Add Project." The "Mindview" library should appear as an option; select that. Press "Finish." Now if you start typing "<Text", Flex Builder should provide "TextDisplay" as a completion option, and when you select that it puts in the necessary namespace field:
I'm taking slight liberties with the name property of the Application to note the name of this MXML file. However, name is generally used for child components so this shouldn't cause any problems.
The creationComplete block is just like a Script block, except that it also defines an event handler -- in this case, the creationComplete event which is fired when the application is finished initializing. Using a creationComplete block is a convenient way to experiment with ActionScript code inside Flex Builder. Note also that the CDATA tags are only necessary when the script block contains special characters.
Although the new component doesn't do much, it helps a little, and often that's all the value you need. In this article it simplifies the code enough to be worth it.
Note that you aren't limited to adding libraries only when creating the project. At any time you can right-click on the project in Flex Builder and add new libraries.
ActionScript is unique among languages in that it is both static and dynamic. Although it is described as being "optionally statically typed," in practice you'll use static typing almost all the time, because that's what works best with Flex Builder, which can do code completion when it knows the types; you'll also get compile-time error checking and more efficient code generation.
However, whenever it suits your needs you can cross the line into dynamic typing, effectively turning off static type checking only for the small portions of your code where you want greater flexibility. This is strikingly powerful, because it means that when you need more dynamic code you aren't forced to jump through the hoops required by a static-only language. Basically, you get the best of both worlds.
In this article, I constantly need to display arrays in a meaningful fashion. I began exploring syntactically clean ways of doing this, and ended up learning about prototypes.
Here is a function for displaying arrays of any dimension, also placed in the com.mindviewinc library. For each element of the array, it checks to see if that element is also an array and if it is, makes a recursive call:
package com.mindviewinc.format {
public function formatArray(a:Array):String {
var result:String = ""
for(var i:int=0; i < a.length; i++) {
if(a[i] is Array)
result += formatArray(a[i])
else if(a[i] == null)
; // Do nothing
else
result += String(a[i])
result += ","
}
// Trim off last comma:
if(result.charAt(result.length - 1) == ',')
result = result.substring(0, result.length - 1)
return "[" + result + "]"
}
}
The is operator performs run-time type identification. If the element is an Array object, we make a recursive call; if it's uninitialized we do nothing (note the need for the semicolon in this case, to define an empty expression), otherwise we convert it to a string. In every case a comma is added, so if you have an array with a specified size that's uninitialized you'll still see a comma for each space.
If there's a trailing comma it's redundant and the Stringsubstring() method removes it. The return expression surrounds the result with square brackets.
In the original JavaScript language, prototypes were the only inheritance mechanism available. Subsequently the more traditional inheritance mechanism has been added and for the most part prototypes are now rarely used, but they still have value because you can add methods dynamically to both objects and classes.
Ruby has the concept of open classes, where any existing class, even a builtin, can be "opened" for further modification. Prototypes are not quite that flexible; they are more like extension methods as found in C# 3.0. In particular, a method added via prototype can only access public elements of the class it's extending.
For example, suppose we start with this:
package com.mindviewinc.test {
public dynamic class TestClass {
internal var x:String = "howdy"
public var y:String = "Yall"
private function foo():String { return "Hello, foo" }
protected function bar():String { return "Hello, bar" }
public function baz():String { return "Hello, baz" }
}
}
The dynamic keyword is necessary if we wish to use prototypes on this class.
To use prototypes, we access the class' prototype field to assign new functions:
The whole thing compiles even without the commented-out lines, but you get the runtime errors shown in the comments. For the function calls, the wording of the errors is not that helpful (it would be more valuable to say "not accessible" rather than "not a function"), but it gets the point across. Here's the output:
f3(): Hello, baz
f4(): null
f5(): Yall
Accessing the x and y fields is a different case. If a field is not public, that field will be created and given an initial value of null. So in that case you won't get a compiler warning or error, you just won't see the value you expect, so bugs are a little harder to track down.
We can now add the show() method to the existing Array class. And whenever this file is included, it will automatically create a TextDisplay named t:
//: Mindview/includes/show.as
// Include this to provide array.show()
import com.mindviewinc.display.TextDisplay
import com.mindviewinc.format.formatArray
var t:TextDisplay = new TextDisplay()
addChild(t)
Array.prototype.setName = function(name:String):void {
this.id = name
this.setPropertyIsEnumerable("id", false)
}
Array.prototype.show = function(msg:String=""):void {
if(msg.length > 0)
t.show(msg, " ")
if(this.hasOwnProperty("id"))
t.show(this.id, ": ")
t.show(formatArray(this))
}
Array.prototype.setPropertyIsEnumerable("setName", false)
Array.prototype.setPropertyIsEnumerable("show", false)
So we don't have to repeat the name of the array every time we display it, the first function attaches a name to the array as the field id. However, if you have an unnamed array, the test for hasOwnProperty() in show() won't try to print it.
By default, methods and fields that you add to an object are "enumerable," which means that a for each statement will produce them. In our Array case, when we call formatArray we only want to see the array elements. Attaching setName() and show() to Array using prototypes will make the new functions enumerable. So that none of these elements show up when displaying the array, we call setPropertyIsEnumerable() and make that property false.
To make this work, we must include this file rather than import it, so that the code is effectively pasted in and compiled as part of the file where it's been included (like a C language #include). I've named the above file show.as and placed it in the Mindview/includes directory. Here's an example where it's used:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="TestShowArray" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
var a:Array = [1,2,3]
a.setName("a")
var b:Array = [[1,2,3],7,[4,5,6]]
b.setName("b")
var c:Array = [b, b]
c.setName("c")
a.show()
b.show()
c.show()
</mx:creationComplete>
</mx:Application>
Here, Array objects are created using the convenient square-bracket syntax. I've given them multiple dimensions in order to verify that formatArray() is working correctly. Here's the output:
Even though setName() and show() are not native methods of Array, their syntax makes them appear so. Although prototypes cannot access anything but public members of a class, which makes them much more limited than Ruby's open classes, they can come in handy when you only need to add a method or two in order to make an existing class callable by a new function.
While developing this article I ran into a problem when trying to create ActionScript functions (rather than classes) inside the Mindview library. I could place the file in the correct folder and give it the right package name, but Flex Builder would not recognize it and do completion, or even compile it if I typed in the import statement by hand.
This is clearly a bug in Flex Builder, but sometimes I could fix it by pretending to create a class in the library, then changing the contents inside the package to a function. Then it was recognized. Unfortunately, this didn't always work; sometimes the only way to make new library components visible is to start a new project after adding the library component. Ideally this bug will be fixed by the time or soon after you read this.
What's called an "array" in ActionScript is generally called a "list" or some other kind of higher-level collection in other languages, because an ActionScript Array object is very full-featured.
You've already seen the square-bracket syntax for creating arrays, which I prefer because it is shorter and cleaner (and is the same as Python). You can also create and initialize array objects using constructor syntax:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="CreatingArrays" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
var a1:Array = new Array() // Zero elements
a1.setName("a1")
a1.show()
var a2:Array = new Array(7) // Seven elements
a2.setName("a2")
a2.show()
a2[4] = 11 // Assign to zero-indexed location 4
a2.show()
var a3:Array = new Array('d', 'e', 'f') // List syntax
a3.setName("a3")
a3.show()
// One way to create multidimensional arrays:
var a4:Array = new Array(a1, a2, a3)
a4.setName("a4")
a4.show()
t.show("a4[1][4]: " + a4[1][4] + " a4[2][2]: " + a4[2][2])
// Using String's split() to make an array of characters:
var a5:Array = "abcdefghijklmnopqrstuvwxyz".split('')
a5.setName("a5")
a5.show()
</mx:creationComplete>
</mx:Application>
Arrays have the kinds of tools you expect for inserting and removing elements. push() and pop() insert and remove elements from the end, and unshift() and shift() insert and remove elements from the beginning. splice() modifies the array in place by simultaneously removing and inserting elements at any point in the array. concat() creates a new array and adds elements to the end, while slice() creates a new array containing a section of the original:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="AddRemoveEtc" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
var a:Array = [1,2,3,4,5,6,7]
a.setName("a")
a.show()
a.push('x') // End of the array
a.show("after push")
a.pop()
a.show("after pop")
a.unshift('y') // Beginning of the array
a.show("after unshift")
a.shift()
a.show("after shift")
// Splice adds and removes from the original array:
a.splice(2, 3, 'a', 'b', 'c', 'd')
a.show("after splice")
// concat() and slice() create new arrays:
var b:Array = a.concat("foo", "bar", "baz")
b.setName("b")
b.show("concatted")
var c:Array = a.slice(1,4)
c.setName("c")
c.show("sliced")
a.show("unchanged")
</mx:creationComplete>
</mx:Application>
Here's the output:
a: [1,2,3,4,5,6,7]
after push a: [1,2,3,4,5,6,7,x]
after pop a: [1,2,3,4,5,6,7]
after unshift a: [y,1,2,3,4,5,6,7]
after shift a: [1,2,3,4,5,6,7]
after splice a: [1,2,a,b,c,d,6,7]
concatted b: [1,2,a,b,c,d,6,7,foo,bar,baz]
sliced c: [2,a,b]
unchanged a: [1,2,a,b,c,d,6,7]
The reverse() method reverses the order of an array in-place. indexOf() searches for the argument and returns the array index of the first occurrence; lastIndexOf() searches backwards from the end:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="ReverseAndSearch" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
var a:Array = "abcdefghijklmnopqrstuvwxyz".split("")
a.setName("a")
a.show()
a.reverse()
a.show("reversed")
// Search using strict equality (===)
a[10] = 'spam'
var i:int = a.indexOf('spam')
t.show("i = a.indexOf('spam'): " + i + ", a[i]: " + a[i])
// Search from the end:
a[20] = 'spam'
a.show()
i = a.lastIndexOf('spam')
t.show("i = a.lastIndexOf('spam'): " + i + ", a[i]: " + a[i])
</mx:creationComplete>
</mx:Application>
Here's the output:
a: [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z]
reversed a: [z,y,x,w,v,u,t,s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a]
i = a.indexOf('spam'): 10, a[i]: spam
a: [z,y,x,w,v,u,t,s,r,q,spam,o,n,m,l,k,j,i,h,g,spam,e,d,c,b,a]
i = a.lastIndexOf('spam'): 20, a[i]: spam
In order to demonstrate the more complex sorting functionality, we need a list of objects with multiple fields. The following Person class contains a static createArray() method that randomly generates any number of Person objects:
package com.mindviewinc.test {
[Bindable] // Create Bindable Value Objects
public class Person {
public var first:String
public var last:String
public var street:String
public var city:String
public var state:String
public var zip:String
public function Person(first:String, last:String, street:String, city:String, state:String, zip:String) {
this.first = first
this.last = last
this.street = street
this.city = city
this.state = state
this.zip = zip
}
private static var values:Array = [
["Bob", "Betty", "John", "Jane", "Alfred", "Annette", "Sam", "Hank", "Hillary"],
["Smith", "Miller", "Johnson", "Jones", "Lee", "Adams", "Jefferson"],
["123 Elm Ave.", "100 Main St.", "123 Broadway", "123 Center St."],
["Springfield", "Oak Grove", "Fairview", "Mount Pleasant", "Centerville", "Riverside", "Greenwood"],
["IL", "UT", "KS", "PA", "IN", "ID", "WA", "OR"]
]
private static function selectRandom(offset:int):String {
return values[offset][Math.round(Math.random() * (values[offset].length - 1))]
}
private static function makeZip(): String {
var result:String = ""
for(var i:int = 0; i < 5; i++)
result += String(Math.round(Math.random() * 9))
return result
}
public static function createPerson():Person {
return new Person(selectRandom(0), selectRandom(1), selectRandom(2),
selectRandom(3), selectRandom(4), makeZip())
}
public static function createArray(size:int):Array {
var result:Array = new Array(size)
for(var i:int = 0; i < size; i++)
result[i] = createPerson()
return result
}
public function toString():String {
return first + " " + last + ", " + street + ", " + city + ", " + state + " " + zip
}
}
}
In the constructor, this is necessary to distinguish the arguments from the fields.
By using the [Bindable] decorator on the class, the objects of this class become Bindable Data Objects, which means that all the fields in the objects become bindable and thus automatically updateable. We'll use this feature later in the article.
Although all the fields are public for simplicity, they could also be exposed as properties. Even nicer, you can transparently change them from public fields into properties without changing any of the client code, because the syntax remains the same.
For convenience, here is a class that can be used as an MXML component to hold an array containing a group of Person objects:
package com.mindviewinc.test {
import mx.core.UIComponent
public class People extends UIComponent {
public var quantity:Number
[Bindable] public var array:Array
protected override function commitProperties():void {
super.commitProperties();
array = Person.createArray(quantity)
}
}
}
The commitProperties() event is called after the properties have been assigned from their MXML declarations, so quantity will be the correct value at that time.
We want to display Person objects in a DataGrid, and to avoid repetition every time we want to control the order the columns are displayed, we'll create an MXML component that inherits from DataGrid:
Notice that you can sort on a particular column by clicking on the header for that column.
Next you can see various kinds of complex sorting using sortOn() to sort on one or more fields, and a sort function:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="ComplexSorting" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
<![CDATA[
include "../../Mindview/includes/show.as"
import com.mindviewinc.test.Person
var people:Array = Person.createArray(10)
people.show = function(msg:String = ""):void {
if(msg.length)
t.show(msg + ":")
for each(var p:* in people)
t.show(p)
t.show("========================================")
}
people.setPropertyIsEnumerable("show", false)
people.show()
people.sort()
people.show("Ordinary sort")
people.sortOn('last')
people.show("sortOn('last')")
people.sortOn(['last', 'street', 'zip'])
people.show("sortOn(['last', 'street', 'zip'])")
people.sortOn(['last', 'zip'], [Array.DESCENDING, Array.NUMERIC | Array.DESCENDING])
people.show("sortOn(['last', 'zip'], [Array.DESCENDING, Array.NUMERIC | Array.DESCENDING]")
function sortOnContinentalDivide(a:Person, b:Person):Number {
// Create set behavior for "in" testing:
var east:Object = {IL:0, KS:0, PA:0, IN:0}
var west:Object = {UT:0, ID:0, WA:0, OR:0}
if(a.state in west && b.state in east)
return 1
if(a.state in east && b.state in west)
return -1
return 0
}
people.sort(sortOnContinentalDivide)
people.show("sortOnContinentalDivide")
]]>
</mx:creationComplete>
</mx:Application>
An ordinary call to sort() just sorts on the initial field in Person, which is first. sortOn() not only allows you to choose a single field to sort on, it allows you to choose multiple fields; the first field in the list is the primary sorting field, the second is the secondary field, and so on. In addition, you can add a parallel array of sorting flags -- each element in the second array affects the corresponding element in the first array.
In general, sortOn() will handle your complex sorting needs, but if it doesn't, you can take complete control by providing a sorting function to the sort() method. This function must return 1, -1 or zero depending on whether the first argument is greater than, less than or equal to the second argument, respectively. In the example above, the sort is based on whether the state is on the east or west side of the continental divide.
Notice the definitions for east and west. These are associative arrays, which you'll learn about shortly. The keys, to the left of the colon, are automatically turned into strings by the compilers. The values are all zero because they are unused in this case. The reason for creating these associative arrays is ActionScript's in keyword, which tells you whether an argument is one of the keys in an associative array. The ability to ask whether an object is part of a group of others is set behavior, so by creating the associative arrays we are conveniently able to ask whether a state is in the east or the west. It's also possible to create associative arrays with objects rather than strings as keys; see later in this article.
Here's the output:
Annette Adams, 123 Elm Ave., Riverside, WA 26323
Annette Miller, 123 Broadway, Springfield, WA 41491
Alfred Jones, 123 Elm Ave., Fairview, UT 43821
Sam Lee, 123 Broadway, Springfield, UT 22643
Hillary Jones, 123 Center St., Riverside, WA 42253
John Miller, 123 Broadway, Oak Grove, ID 26276
John Adams, 123 Elm Ave., Centerville, WA 22253
Sam Adams, 123 Center St., Mount Pleasant, KS 50456
Betty Miller, 123 Broadway, Riverside, IN 05445
Annette Jones, 123 Elm Ave., Fairview, PA 26193
========================================
Ordinary sort:
Alfred Jones, 123 Elm Ave., Fairview, UT 43821
Annette Adams, 123 Elm Ave., Riverside, WA 26323
Annette Jones, 123 Elm Ave., Fairview, PA 26193
Annette Miller, 123 Broadway, Springfield, WA 41491
Betty Miller, 123 Broadway, Riverside, IN 05445
Hillary Jones, 123 Center St., Riverside, WA 42253
John Adams, 123 Elm Ave., Centerville, WA 22253
John Miller, 123 Broadway, Oak Grove, ID 26276
Sam Adams, 123 Center St., Mount Pleasant, KS 50456
Sam Lee, 123 Broadway, Springfield, UT 22643
========================================
sortOn('last'):
Sam Adams, 123 Center St., Mount Pleasant, KS 50456
Annette Adams, 123 Elm Ave., Riverside, WA 26323
John Adams, 123 Elm Ave., Centerville, WA 22253
Annette Jones, 123 Elm Ave., Fairview, PA 26193
Hillary Jones, 123 Center St., Riverside, WA 42253
Alfred Jones, 123 Elm Ave., Fairview, UT 43821
Sam Lee, 123 Broadway, Springfield, UT 22643
John Miller, 123 Broadway, Oak Grove, ID 26276
Betty Miller, 123 Broadway, Riverside, IN 05445
Annette Miller, 123 Broadway, Springfield, WA 41491
========================================
sortOn(['last', 'street', 'zip']):
Sam Adams, 123 Center St., Mount Pleasant, KS 50456
John Adams, 123 Elm Ave., Centerville, WA 22253
Annette Adams, 123 Elm Ave., Riverside, WA 26323
Hillary Jones, 123 Center St., Riverside, WA 42253
Annette Jones, 123 Elm Ave., Fairview, PA 26193
Alfred Jones, 123 Elm Ave., Fairview, UT 43821
Sam Lee, 123 Broadway, Springfield, UT 22643
Betty Miller, 123 Broadway, Riverside, IN 05445
John Miller, 123 Broadway, Oak Grove, ID 26276
Annette Miller, 123 Broadway, Springfield, WA 41491
========================================
sortOn(['last', 'zip'], [Array.DESCENDING, Array.NUMERIC | Array.DESCENDING]:
Annette Miller, 123 Broadway, Springfield, WA 41491
John Miller, 123 Broadway, Oak Grove, ID 26276
Betty Miller, 123 Broadway, Riverside, IN 05445
Sam Lee, 123 Broadway, Springfield, UT 22643
Alfred Jones, 123 Elm Ave., Fairview, UT 43821
Hillary Jones, 123 Center St., Riverside, WA 42253
Annette Jones, 123 Elm Ave., Fairview, PA 26193
Sam Adams, 123 Center St., Mount Pleasant, KS 50456
Annette Adams, 123 Elm Ave., Riverside, WA 26323
John Adams, 123 Elm Ave., Centerville, WA 22253
========================================
sortOnContinentalDivide:
Sam Adams, 123 Center St., Mount Pleasant, KS 50456
Annette Jones, 123 Elm Ave., Fairview, PA 26193
Betty Miller, 123 Broadway, Riverside, IN 05445
John Miller, 123 Broadway, Oak Grove, ID 26276
Alfred Jones, 123 Elm Ave., Fairview, UT 43821
Annette Miller, 123 Broadway, Springfield, WA 41491
Sam Lee, 123 Broadway, Springfield, UT 22643
Hillary Jones, 123 Center St., Riverside, WA 42253
Annette Adams, 123 Elm Ave., Riverside, WA 26323
John Adams, 123 Elm Ave., Centerville, WA 22253
========================================
Like Java, ActionScript is reference-based. If you point two references at the same object, any changes in the object will show up through both references:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="CopyingArrays" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
function display():void {
t.show("a: " + formatArray(a))
t.show("b: " + formatArray(b))
}
var a:Array = ['a', 'b', 'c']
var b:Array = a
display()
a[1] = 'x'
display()
b = a.slice()
b[1] = 'b'
display()
var c:Array = b.concat()
c[2] = 'z'
display()
t.show("c: " + formatArray(c))
</mx:creationComplete>
</mx:Application>
Changing a[1] changes it for both a and b when the two references are aliased to the same array object. But using either slice() or concat() with no arguments produces a shallow copy* of the array, which means that the array is duplicated, but the objects the array points to are not.
A deep copy means that not only are references copied, but all the objects the references are pointing to must also be copied. To see the effect of shallow vs. deep copy, it's helpful to have a class that automatically generates a unique field:
package com.mindviewinc.test {
public class AutoID {
private static var count:Number = 'A'.charCodeAt()
public var id:String = String.fromCharCode(count++)
public function toString():String { return id }
}
}
Using a static variable, this class automatically creates a different id for each new object, and produces that id whenever you ask for a string representation of the object. Notice that the override keyword is not used when defining toString() even though you normally need to invoke override when providing a new definition for an existing method. This is an artifact left over from prototype days, and it will cause you some consternation if you forget and try to use override.
The following example demonstrates the issue with shallow copy, and then shows the solution suggested in the ActionScript documentation (which comes with Flex Builder):
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="ShallowCopy" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
import com.mindviewinc.test.AutoID
function display():void {
t.show("a: " + formatArray(a))
t.show("b: " + formatArray(b))
}
var a:Array = [new AutoID(), new AutoID(), new AutoID(), new AutoID()]
var b:Array = a.slice() // Shallow copy
display()
a[3].id = 'X'
display() // Objects being pointed to are the same
// Solution suggested in ActionScript docs:
import flash.utils.ByteArray
function deepCopy(source:Object):* {
var myBA:ByteArray = new ByteArray()
myBA.writeObject(source)
myBA.position = 0
return(myBA.readObject())
}
// The bits are there, but it loses the type information:
b = deepCopy(a)
a[3].id = 'Y'
display()
for(var i:* in b)
t.show('b[i].id: ' + b[i].id)
</mx:creationComplete>
</mx:Application>
The deepCopy() function creates a deep copy by serializing the array into an instance of the ByteArray class, and then reading the array back into a new array.
The result of deepCopy() isn't quite right, as you can see from the output:
a: [A,B,C,D]
b: [A,B,C,D]
a: [A,B,C,X]
b: [A,B,C,X]
a: [A,B,C,Y]
b: [[object Object],[object Object],[object Object],[object Object]]
b[i].id: A
b[i].id: B
b[i].id: C
b[i].id: X
Although the id fields are there, this approach loses the type information about what the object is.
Associative arrays are also referred to as dictionaries or maps. A regular array uses numbers to index into the array, whereas an associative array uses objects as indexes (called keys).
While C++ and Java use libraries for associative arrays, in Python, Ruby and ActionScript they are first-class language elements (in Perl they are hashes). The ActionScript/JavaScript approach seems slightly odd because an associative array is just an Object, or rather an Object is an associative array. It turns out this is not so strange; in Python, for example, all members of an object are held in a dictionary.
ActionScript provides a convenient syntax for creating dictionaries, very similar to Python's dictionary definition. You define the dictionary using curly braces, and divide keys from values using colons:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="AssociativeArrays" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
var dict:Object = { foo:"hello", bar:"world", baz: "goodbye" }
t.show("dict.foo: " + dict.foo)
t.show("dict['bar']: " + dict['bar'])
for(var key:String in dict)
t.show(key + ": " + dict[key])
for each(var val:String in dict)
t.show(val)
</mx:creationComplete>
</mx:Application>
Here's the output:
dict.foo: hello
dict['bar']: world
baz: goodbye
bar: world
foo: hello
goodbye
world
hello
Notice that, even though we're using dictionary-definition syntax, we are actually creating an object, as you can see by the dot-access in dict.foo. But you can also access elements using array-indexing syntax as seen in dict['bar'], although you must put the key in quotes (even though quotes are not necessary when defining the key within curly braces).
You can now see the fundamental difference between the for and for each statements: both iterate through the elements of an object, but for produces the keys and for each produces the values. This explains the odd results when using for and for each with Array objects:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="ForAndForEach" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
var a:Array = "AEIOU".split('')
for(var k:* in a)
t.show(k + " typeof k: " + typeof k)
for each(var v:* in a)
t.show(v + " typeof v: " + typeof v)
</mx:creationComplete>
</mx:Application>
The k:* is declaring k as an unknown type, since this is a generic function and we don't know what's in the array.
When you say for, you're just iterating through the keys of the Array, which are just the numerical indices:
0 typeof k: number
1 typeof k: number
2 typeof k: number
3 typeof k: number
4 typeof k: number
A typeof v: string
E typeof v: string
I typeof v: string
O typeof v: string
U typeof v: string
Because the curly-brace syntax produces an object and not just a plain dictionary, it is also a syntax for creating anonymous objects:
Hello, anonymous world
1066
Hello, anonymous world
10660
Note that anon also exhibits closure behavior; it captures the value of times which is defined outside of the scope of anon. This is not, however, a full proof of closure behavior in ActionScript; here is an article on closures that gives you the details.
In the original example in this section, the dictionary was defined like this:
var dict:Object = { foo:"hello", bar:"world", baz: "goodbye" }
This syntax automatically turns the keys into strings, even though they are not quoted. But in the general case you are not restricted to strings as keys; you can use any object as a key.
The following example not only shows the use of objects as keys, but it also introduces the Dictionary class, which has a somewhat subtle difference from using the basic Object dictionary. This reuses com.mindviewinc.test.AutoID, seen earlier in this article:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="ObjectsAsKeys" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
import com.mindviewinc.test.AutoID
function display(x:Object):void {
for(var k:* in x)
t.show(k + ":" + x[k] + ", typeof k: " + typeof k +
", typeof x[k]: " + typeof x[k])
}
var a:AutoID = new AutoID(), b:AutoID = new AutoID(), c:AutoID = new AutoID()
var dict1:Object = {a:'x', b:'y', c:'z'}
display(dict1)
t.show("dict1[b]:" + dict1[b]) // Undefined
var dict2:Object = new Object()
dict2[a] = 'one'
dict2[b] = 'two'
dict2[c] = 'three'
display(dict2)
t.show("dict2[b]:" + dict2[b])
var dict3:Dictionary = new Dictionary()
dict3[a] = 'one'
dict3[b] = 'two'
dict3[c] = 'three'
display(dict3)
t.show("dict3[b]:" + dict3[b])
</mx:creationComplete>
</mx:Application>
dict1 uses the "convenience syntax," but this ignores the attempt to use the objects a, b and c. Instead it turns the key names into strings, so when we say dict1[b] it doesn't have a value for b the object as a key.
With dict2 we explicitly assign using the square-bracket syntax and the un-quoted a, b and c. This time it does use the objects as keys, but you can see from the output that it uses their string representation (which is oftentimes just fine). Dictionary uses the key object's identity rather than its string representation, which you can see from the typeof expression in dict3's output.
When you run this, you'll get an alert that says "true."
If you have data that changes, the raw Array object doesn't generate change notifications to the data provider component, so that component doesn't get updated until it must be redrawn or if the data provider is reassigned. The ArrayCollection, however, generates these notifications. Thus, you should use an ArrayCollection to explicitly wrap a raw Array:
Notice the different approaches taken by each button: the first refreshes the ArrayCollection, the second invalidates the list in the PeopleGrid. In both cases the data on the PeopleGrid is updated, but the refresh causes a "push" from the ArrayCollection to the PeopleGrid.
We'll often want our arrays of Person objects to be contained within ArrayCollections, so here's a component that encapsulates the process:
package com.mindviewinc.test {
import mx.collections.ArrayCollection
import mx.core.UIComponent
public class PeopleAC extends UIComponent {
public var quantity:Number
[Bindable] public var collection:ArrayCollection
private var array:Array
protected override function commitProperties():void {
super.commitProperties()
array = Person.createArray(quantity)
collection = new ArrayCollection(array)
}
}
}
This example shows many of the operations available for the ArrayCollection, but not all -- the remainder you can find in the Flex Builder documentation or online. A DataGrid of Person objects serves to illuminate these operations:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="ArrayCollectionOperations"
xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:test="com.mindviewinc.test.*"
xmlns:display="com.mindviewinc.display.*">
<mx:Script>
import com.mindviewinc.test.Person
import mx.collections.*
[Bindable]
private var people:ArrayCollection = new ArrayCollection(Person.createArray(40))
private function filter(item:Object):Boolean {
return item.city == "Springfield"
}
private function makeSort():Sort {
var mySort:Sort = new Sort()
// Sort on the state first, last name second.
mySort.fields = [new SortField("state",true), new SortField("last",true)]
return mySort
}
</mx:Script>
<mx:HDividedBox width="100%" height="100%">
<mx:VBox height="100%">
<mx:Button label="Add Filter" click="people.filterFunction=filter; people.refresh()"/>
<mx:Button label="Remove Filter" click="people.filterFunction=null; people.refresh()"/>
<mx:Button label="Add Sort" click="people.sort=makeSort(); people.refresh()"/>
<mx:Button label="Remove Sort" click="people.sort=null; people.refresh()"/>
<mx:Button label="Append" click="people.addItem(Person.createPerson())"/>
<mx:Button label="Remove Item" click="people.removeItemAt(0)"/>
<mx:Button label="Remove All" click="people.removeAll()"/>
<mx:Button label="Add At #3" click="people.addItemAt(Person.createPerson(), 3)"/>
<mx:Button label="Set Item #3" click="people.setItemAt(Person.createPerson(), 3)"/>
<mx:Button label="Get Item #3" click="t.text=String(people.getItemAt(3))"/>
<display:TextDisplay id="t" width="100%" height="40"/>
</mx:VBox>
<test:PeopleGrid dataProvider="{people}"/>
</mx:HDividedBox>
</mx:Application>
Notice that when you apply or remove sorts and filters, you must call refresh() in order for the results to propagate to the display component, but all other operations automatically propagate.
Although there's only a single method that pertains to cursors, createCursor() returns an IViewCursor that has some interesting properties and methods:
In general, you can do a lot without cursors so you may not see them used that often. However, if you write code that uses cursors then it works with both ArrayCollection and XMLListCollection.
ArrayCollections are primarily useful because they are Flex components that wrap Arrays and provide binding and events. The Array, being a Flash player component, isn't directly bindable and doesn't issue its own events. However, the underlying Array that each ArrayCollection wraps has some very interesting methods, a number of which fall under the category of functional programming.
Functional programming is applying operations to sequences. These operations, however, are not applied in the usual way, looping through the sequence and doing things one at a time. In functional programming, you give directions to be applied to the sequence en masse. So, for example, you say sequence.sum() rather than looping through and adding each element.
An important aspect of functional programming is that each of these operations have no side effects; that is, they do not affect either the sequence they are operating on or the surrounding environment. A pure functional operation only produces a result. Because of this constraint, each function call can be given its own thread and it doesn't collide with other threads -- thus functional programming provides an inherently thread-safe programming model.
The no side effects rule is often something that must be followed, rather than enforced by the programming system. And in a language like Flex, which does not (yet) have concurrent programming support, the style is often followed more than the principles. For example, one of the more common operations for Arrays is forEach(). On its own, forEach() has no way to be a pure functional call:
[John LEE, 123 Center St., Riverside, WA 84712,Bob JOHNSON, 123 Center St., Oak Grove, ID 11612,
Annette JOHNSON, 100 Main St., Centerville, ID 96663,Alfred SMITH, 123 Elm Ave., Springfield, PA 96655,
Sam SMITH, 123 Center St., Riverside, KS 14278,Betty JONES, 123 Elm Ave., Fairview, ID 02329,
Alfred JOHNSON, 123 Broadway, Riverside, OR 95528]
forEach() takes a function that it applies to each element in the Array. It passes in the current element, the index, and the Array. Thus, forEach() has no context in which to capture intermediate results, so it cannot create and return a list of capitalized names. However, you can use forEach() within your own function, in which you establish this context yourself:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="CapitalizeResults" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
import com.mindviewinc.test.Person
function capitalizeLast(a:Array):Array {
var result:Array = new Array(a.length)
a.forEach(function(elem:*, index:int, x:Array):void {
result[index] = String(elem.last).toUpperCase()
})
return result
}
var people:Array = Person.createArray(7)
capitalizeLast(people).show()
people.show()
</mx:creationComplete>
</mx:Application>
Now capitalizeLast() follows the no side effect rule and only returns a new Array, but does not affect the source Array as you can see from the results:
[MILLER,SMITH,JOHNSON,JONES,JOHNSON,ADAMS,JONES]
[Annette Miller, 123 Center St., Fairview, ID 12877,Betty Smith, 123 Center St., Oak Grove, KS 55622,
Betty Johnson, 123 Broadway, Fairview, WA 34446,Alfred Jones, 123 Center St., Centerville, OR 74145,
Annette Johnson, 123 Center St., Fairview, IN 27773,John Adams, 100 Main St., Centerville, IL 65176,
Hank Jones, 123 Broadway, Riverside, WA 88713]
One of the more powerful functional operations is map(), because it constructs its own new result array (as we did above) and applies the argument function on each element in the source array to produce the elements in the result array. So we can rewrite the previous example like so:
You'll see similar output as before, but note that the result of map()s argument function is not void this time; instead, it's a String and map() stores each intermediate value in an Array that it keeps to produce the result. So we didn't have to write our own capitalizeLast() function as we did before.
In general, when you need to take an Array and produce another Array from it, you'll use map(), and you will only use forEach() when you do want to modify the source Array.
Array has two built-in functions that apply a Boolean test to every element in the Array and return a Boolean result. every() produces true if every element in the Array passes the test, and some() returns true if one or more elements pass the test:
You can easily write your own and add them to a library for reuse. Here are a few examples, but you can easily add more. For inspiration, look at Python's itertools library and examples you can find around the web using itertools.
range() produces a sequence of numbers. Python's range() is defined as range([start,] stop[, step]) where start is the starting value, stop is the value to stop at, and step is the increment value. Because start and stop are optional, we have to look at the actual number of arguments to decide what the arguments mean:
package com.mindviewinc.functional {
public function range(...args):Array {
function assert(b:Boolean, msg:String=""):void {
var errorMsg:String = "Error in in call to range()"
if(msg.length > 0)
errorMsg += ": " + msg
if(!b)
throw new ArgumentError(errorMsg)
}
assert(args.length > 0, "At least one argument required")
assert(args.length <= 3, "Too many arguments")
for(var i:int = 0; i < args.length; i++)
assert(args[i] is Number, "All arguments must be Numbers")
var start:int = 0, stop:int = 0, stride:int = 1
if(args.length == 1)
stop = args[0]
if(args.length == 2) {
start = args[0]
stop = args[1]
}
if(args.length == 3) {
start = args[0]
stop = args[1]
stride = args[2]
}
var result:Array = [];
if(stride == 0) return result
if((stop - start < 0) != (stride < 0)) return result
for(i = start; i < stop; i += stride)
result.push(i)
return result
}
}
The "..." syntax indicates a variable argument list.
Notice the nested assert() function. Flex allows only one function to be exposed in a package, and it doesn't allow the use of any other access specifiers for functions (no specifier exposes the function), so the only way I could figure out how to add more than one function is to nest it, which worked nicely. Since assert() ended up only checking arguments, I used the more specific exception ArgumentError.
To follow Python functionality, if any of the argument values doesn't make sense, range() returns an empty Array.
reduce() is another function lifted from Python. It takes an Array and applies a reduction function to every element, finally producing a single result:
package com.mindviewinc.functional {
public function reduce(a:Array, reducer:Function):* {
var result:* = a[0]
for each(var item:* in a.slice(1, a.length))
result = reducer(result, item)
return result
}
}
Originally I tried to declare reducer as a function that takes two arguments, but discovered that this completely confused the compiler (unfortunately, it gave me no error messages, just lost its mind). It turns out that to pass in a function, you just use the type Function with no details, so here we're in the land of partial duck typing (dynamic type checking). It requires a function, but you can't specify that function's arguments, so we call reducer() with whatever arguments and return types that we expect, and we'll get an exception at runtime if it's used incorrectly.
Note that I'm careful not to modify the argument a; I just read from it -- this follows the no side effects rule.
Here's a simple test that just sums up the values:
Python's zip() returns a list of tuples (read-only sequences) where each tuple contains the nth element of each of the argument sequences. We don't have tuples in ActionScript, so our zip() will just create Arrays.
If the input arrays are of different sizes, the result is the length of the smallest input array:
package com.mindviewinc.functional {
public function zip(...arrays):Array {
// Find the shortest array:
var smallest:int =
arrays.map(function(e:*, i:int, a:Array):int {
return e.length
}).sort()[0];
var result:Array = new Array(smallest)
for(var i:int = 0; i < result.length; i++) {
var tuple:Array = []
for(var a:* in arrays) {
tuple.push(arrays[a][i])
}
result[i] = tuple
}
return result
}
}
Note that the complexity of the smallest initialization expression requires a semicolon.
Here's a test that demonstrates the behavior of zip():
One functional-programming subject I haven't discussed is composability, which means that you can take existing functions and combine them into new functions. This means that the thread-independence characteristics of the existing functions still exist in the new function, which can be run in pieces under independent threads. Function composition tends to get complex, and if you don't get the concurrency benefits (as we don't in Flex -- yet) then it's generally more trouble than it's worth.
For me the best feature of the Actionscript language is its ability to allow class declarations as dynamic or static. But i dont understand why it doesnt support function overloading.
>> you'll also get compile-time error checking and more efficient code generation
Can this be proved, or is there any official documents that shows statically typed code will generate more effective code? From what I saw, there are only few differences between the ABC IL generated whether it's explicitly typed or not. The runtime typechecking was never omitted in IL; even if the JITter knows how to get rid of redundant checkings, that probably has nothing to do with explicitly typing the code or not.
"If a field is not public, that field will be created and given an initial value of null".
Actually, the default value of a noneexisting property in dynamic class is undefined. In the example it's casted to the function return type (Object) and thus results in null. If you'd chage it to * instead, the value will stay undeined
I really enjoyed reading your artice, and looking up the examples!
> >> you'll also get compile-time error checking and more > efficient code generation > > Can this be proved, or is there any official documents > that shows statically typed code will generate more > effective code?
The question becomes more interesting when you throw jitting into it -- I'm not sure if the JIT does anything with the type information but I suspect so.
As far as static type checking in general, I was primarily reiterating "known stuff" here for languages in general, but James Ward did comment to me once that the ActionScript compiler generated more efficient code when the type is known.
If a JIT or runtime optimizer can figure out the type of an unspecified object, it will almost always produce more efficient code -- but again, that's based on more specific type knowledge. If you are able to give it the type ahead of time, which Flex Builder/the Flex compiler encourage, the compiler can create more efficient code with it.
> >> you'll also get compile-time error checking and more > efficient code generation > > Can this be proved, or is there any official documents > that shows statically typed code will generate more > effective code? From what I saw, there are only few > differences between the ABC IL generated whether it's > explicitly typed or not. The runtime typechecking was > never omitted in IL; even if the JITter knows how to get > rid of redundant checkings, that probably has nothing to > do with explicitly typing the code or not.
Regarding the loss of type information when using the byte array method of deep copying. Since the objects are serialised into the byte array as AMF the [RemoteClass] metadata tag is honoured, so if a the objects being copied in this way have that tag type information is preserved and the copies are strongly typed. More here: http://www.lineartime.co.uk/blog/?p=8#more-8
"Notice the nested assert() function. Flex allows only one function to be exposed in a package, and it doesn't allow the use of any other access specifiers for functions (no specifier exposes the function), so the only way I could figure out how to add more than one function is to nest it, which worked nicely."
Actionscript allows only one public construct per file, but it will let you define anything else (not public) you like outside of the package body, and any such code will be placed in the global scope:
package { // Code here is in the global scope } // Code here is also in the global scope
Thus, the range() defn above could be equally written with assert() factored out into the file's global scope, like so:
package com.mindviewinc.functional { public function range(...args):Array { ... assert(args.length <= 3, "Too many arguments") ... } } function assert(b:Boolean, msg:String=""):void { ... }