The Artima Developer Community
Sponsored Link

Computing Thoughts
ActionScript Collections and Functional Programming
by Bruce Eckel
June 5, 2008
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.

Advertisement

Contents

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.

Creating and Using Libraries

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.

Library Structure

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.

Creating a Library

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:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="TestTextDisplay"
    xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:display="com.mindviewinc.display.*" >
<display:TextDisplay id="t"/>
<mx:creationComplete>
t.show("hello", ", ")
t.show("world")
</mx:creationComplete>
</mx:Application>

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.

Prototypes for Dynamic Method Addition

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.

Displaying Arrays

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 String substring() method removes it. The return expression surrounds the result with square brackets.

Improving the Syntax

If we use formatArray() with a TextDisplay object, the syntax for displaying an a:Array is:

t.show("a: " + formatArray(a))

Which is tolerable but tedious. It would be much nicer to be able to say:

a.show()

And have everything else taken care of for us. But this would appear to require us to add a new show() method to the Array class.

Prototypes

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:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="TestPrototypes" xmlns:mx="http://www.adobe.com/2006/mxml"
    xmlns:test="com.mindviewinc.test.*" xmlns:display="com.mindviewinc.display.*">
<display:TextDisplay id="t"/>
<mx:creationComplete>
    import com.mindviewinc.test.TestClass
    TestClass.prototype.f1 = function():String { return this.foo() }
    TestClass.prototype.f2 = function():String { return this.bar() }
    TestClass.prototype.f3 = function():String { return this.baz() }
    TestClass.prototype.f4 = function():String { return this.x }
    TestClass.prototype.f5 = function():String { return this.y }
    var tc:TestClass = new TestClass()
    // t.show("f1(): " + tc.f1()) // Runtime error: "foo is not a function"
    // t.show("f2(): " + tc.f2()) // Runtime error: "bar is not a function"
    t.show("f3(): " + tc.f3())
    t.show("f4(): " + tc.f4())
    t.show("f5(): " + tc.f5())
</mx:creationComplete>
</mx:Application>

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.

Improving the Syntax Using Prototypes

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:

a: [1,2,3]
b: [[1,2,3],7,[4,5,6]]
c: [[[1,2,3],7,[4,5,6]],[[1,2,3],7,[4,5,6]]]

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.

Aside: Placing Functions into the Library

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.

The Versatile Array

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.

Creating Arrays

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>

Here's the output:

a1: []
a2: [,,,,,,]
a2: [,,,,11,,]
a3: [d,e,f]
a4: [[],[,,,,11,,],[d,e,f]]
a4[1][4]: 11 a4[2][2]: f
a5: [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]

Adding, Removing, Concat, Slicing & Splicing

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]

Reversing and Searching

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

Basic Sorting

Basic sorting uses built-in comparisons. You can also specify variations on the basic sort using flags, which can be ORed together:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="Sorting" 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.sort()
a.show("After sorting")
a.sort(Array.CASEINSENSITIVE)
a.show("After case-insensitive sort")
a.sort(Array.CASEINSENSITIVE | Array.DESCENDING)
a.show("After case-insensitive, descending sort")
var b:Array = [4,5,6,7,8,9,10]
b.setName("b")
b.show()
b.sort()
b.show("After sorting")
b.sort(Array.NUMERIC)
b.show("After numeric sort")
b.sort(Array.NUMERIC | Array.DESCENDING)
b.show("After numeric, descending sort")
var c:Array = "ABCDEFGHIJK".split("").reverse()
c.setName("c")
c.show()
var csorted:Array = c.sort(Array.RETURNINDEXEDARRAY)
c.show("After indexed array sort")
csorted.setName("csorted")
csorted.show()
</mx:creationComplete>
</mx:Application>

Most of these sorts happen in-place. 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]
After sorting a: [B,D,F,H,J,L,N,P,R,T,V,X,Z,a,c,e,g,i,k,m,o,q,s,u,w,y]
After case-insensitive sort 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]
After case-insensitive, descending sort 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]
b: [4,5,6,7,8,9,10]
After sorting b: [10,4,5,6,7,8,9]
After numeric sort b: [4,5,6,7,8,9,10]
After numeric, descending sort b: [10,9,8,7,6,5,4]
c: [K,J,I,H,G,F,E,D,C,B,A]
After indexed array sort c: [K,J,I,H,G,F,E,D,C,B,A]
csorted: [10,9,8,7,6,5,4,3,2,1,0]

Complex Sorting

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:

<?xml version="1.0" encoding="utf-8"?>
<mx:DataGrid name="PeopleGrid" height="100%" editable="true" xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:columns>
        <mx:DataGridColumn dataField="first"/>
        <mx:DataGridColumn dataField="last"/>
        <mx:DataGridColumn dataField="street"/>
        <mx:DataGridColumn dataField="city"/>
        <mx:DataGridColumn dataField="state"/>
        <mx:DataGridColumn dataField="zip"/>
    </mx:columns>
</mx:DataGrid>

The columns group establishes the display order for the data columns. As a simple test, we display a list of Person objects in a PeopleGrid:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="PersonTest" xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:test="com.mindviewinc.test.*">
<test:People id="people" quantity="40"/>
<test:PeopleGrid dataProvider="{people.array}"/>
</mx:Application>

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
========================================

Copying Arrays

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>

This produces the following output:

a: [a,b,c]
b: [a,b,c]
a: [a,x,c]
b: [a,x,c]
a: [a,x,c]
b: [a,b,c]
a: [a,x,c]
b: [a,b,c]
c: [a,b,z]

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.

Deep Copy

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

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:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="AnonymousObjects" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
var times:Number = 10
var anon:Object = {
    field1: "Hello, anonymous world",
    field2: 1066,
    function1: function():String { return this.field1 },
    function2: function():Number { return times * this.field2}
}
t.show(anon.field1)
t.show(anon.field2)
t.show(anon.function1())
t.show(anon.function2())
</mx:creationComplete>
</mx:Application>

This produces:

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.

Objects as Keys

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>

The output tells the story:

b:y, typeof k: string, typeof x[k]: string
c:z, typeof k: string, typeof x[k]: string
a:x, typeof k: string, typeof x[k]: string
dict1[b]:undefined
B:two, typeof k: string, typeof x[k]: string
A:one, typeof k: string, typeof x[k]: string
C:three, typeof k: string, typeof x[k]: string
dict2[b]:two
A:one, typeof k: object, typeof x[k]: string
B:two, typeof k: object, typeof x[k]: string
C:three, typeof k: object, typeof x[k]: string
dict3[b]:two

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.

ArrayCollection

It turns out that even if you use a raw Array as a dataProvider, that Array is automatically wrapped in an ArrayCollection:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="ArrayCollectionWrapping" xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:test="com.mindviewinc.test.*">
<test:People id="people" quantity="40"/>
<test:PeopleGrid id="dg1" dataProvider="{people.array}"/>
<mx:creationComplete>
import mx.collections.ArrayCollection
import mx.controls.Alert
Alert.show(String(dg1.dataProvider is ArrayCollection))
</mx:creationComplete>
</mx:Application>

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:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="ArrayCollectionNotification"
    xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:test="com.mindviewinc.test.*">
<test:People id="people" quantity="18"/>
<mx:ArrayCollection id="ac1" source="{people.array}"/>
<mx:ArrayCollection id="ac2" source="{people.array}"/>
<test:PeopleGrid id="dg1" dataProvider="{ac1}"/>
<test:PeopleGrid id="dg2" dataProvider="{ac2}"/>
<mx:Button label="delete last item"
    click="people.array.pop(); ac1.refresh(); ac2.refresh()"/>
<mx:Button label="delete last item (2)"
    click="people.array.pop();dg1.invalidateList();dg2.invalidateList()"/>
</mx:Application>

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)
        }
    }
}

ArrayCollection Operations

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.

Cursors (Iterators)

Although there's only a single method that pertains to cursors, createCursor() returns an IViewCursor that has some interesting properties and methods:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="ArrayCollectionCursors"
    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(20));
[Bindable]
private var cursor:IViewCursor
private var bookmark:CursorBookmark
private function update():void {
    if(!cursor) return
    t.text = String(cursor.current)
    before.text = "cursor.beforeFirst: " + cursor.beforeFirst
    after.text = "cursor.afterLast: " + cursor.afterLast
}
private function findBob():void {
    // findLast() and findAny() also require a sorted ArrayCollection.
    people.sort = new Sort()
    people.sort.fields = [new SortField("first",true)]
    people.refresh()
    cursor = people.createCursor()
    cursor.findFirst({first:"Bob"})
    update()
}
</mx:Script>
<mx:HDividedBox width="100%" height="100%">
    <mx:VBox height="100%">
        <mx:Button label="Get Cursor" click="cursor=people.createCursor();update()"/>
        <mx:Button label="Cursor Forward" click="if(cursor) cursor.moveNext();update()"/>
        <mx:Button label="Cursor Backward" click="if(cursor) cursor.movePrevious();update()"/>
        <mx:Button label="Remove" click="if(cursor) cursor.remove();update()"/>
        <mx:Button label="Insert" click="if(cursor) cursor.insert(Person.createPerson());update()"/>
        <mx:Button label="Find Bob" click="findBob()"/>
        <mx:Button label="Set Bookmark" click="if(cursor) bookmark=cursor.bookmark"/>
        <mx:Button label="Clear Sort" click="people.sort=null;people.refresh()"/>
        <mx:Button label="Goto Bookmark" click="if(cursor) cursor.seek(bookmark);update()"/>
        <display:TextDisplay id="t" width="100%" height="40"/>
        <mx:Label id="before" />
        <mx:Label id="after" />
    </mx:VBox>
    <test:PeopleGrid dataProvider="{people}"/>
</mx:HDividedBox>
</mx:Application>

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.

Functional Programming

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:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="CapitalizeInSitu" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
import com.mindviewinc.test.Person
var people:Array = Person.createArray(7)
people.forEach(function(elem:*, index:int, a:Array):void {
    a[index].last = String(elem.last).toUpperCase()
})
people.show()
</mx:creationComplete>
</mx:Application>

You'll see a result that looks like this:

[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]

map()

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:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="MapCapitalize" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
import com.mindviewinc.test.Person
var people:Array = Person.createArray(7)
people.map(function(elem:*, index:int, x:Array):String {
    return String(elem.last).toUpperCase()
}).show()
people.show()
</mx:creationComplete>
</mx:Application>

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.

filter()

filter() also produces a new Array, but it does so by testing each element to find out if it belongs in the result.

We can use filter() to find all the people with the last name "Miller":

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="FilterMiller" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
import com.mindviewinc.test.Person
var people:Array = Person.createArray(100)
t.show(people.filter(function(elem:*, index:int, x:Array):Boolean {
    return elem.last == "Miller"
}).length)
</mx:creationComplete>
</mx:Application>

Boolean Tests

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:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="BooleanFunctional" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
var functions:Array = [
    function(e:*, i:int, a:Array):Boolean {
        return e.length == 3
    },
    function(e:*, i:int, a:Array):Boolean {
        return e.indexOf('a') != -1
    },
    function(e:*, i:int, a:Array):Boolean {
        return e.indexOf('b') != -1
    },
]
var a:Array = ['aaa', 'aab', 'abb', 'bba', 'xax', 'yya']
for each(var func:Function in functions) {
    t.show(a.every(func))
    t.show(a.some(func), "\n\n")
}
</mx:creationComplete>
</mx:Application>

Here I've created an Array of functions which is applied to the Array a using for each. Here's the output:

true
true

true
true

false
true

Creating Your Own Functional Operations

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()

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.

Here's a test program:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="RangeTest" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
import com.mindviewinc.functional.range;
range(10).show()
range(10, 20).show()
range(10, 100, 5).show()
range(-10, 100, 5).show()
range(-10).show()
range(0).show()
range(-10, -20).show()
range(-20, -10).show()
range(-20, -10, 2).show()
range(-20, -10, -2).show()
</mx:creationComplete>
</mx:Application>

And the output:

[0,1,2,3,4,5,6,7,8,9]
[10,11,12,13,14,15,16,17,18,19]
[10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95]
[-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95]
[]
[]
[]
[-20,-19,-18,-17,-16,-15,-14,-13,-12,-11]
[-20,-18,-16,-14,-12]
[]

reduce()

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:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="ReduceSum" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
import com.mindviewinc.functional.*
t.show(reduce(range(100), function(e1:*, e2:*):* {
    return e1 + e2
}))
</mx:creationComplete>
</mx:Application>

You'll see a result of 4950, which is what Python produces with the statement sum(range(100)).

Of course, you can also make your own sum() function and add it to your library.

zip()

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():

<?xml version="1.0" encoding="utf-8"?>
<mx:Application name="ZipTest" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:creationComplete>
include "../../Mindview/includes/show.as"
import com.mindviewinc.functional.zip
var a:Array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
a.setName("a")
var b:Array = "abcdefghijkl".split('')
b.setName("b")
var c:Array = "qrstuvwxyz".split('')
c.setName("c")
a.show()
b.show()
c.show()
zip().show()
zip(a).show()
zip(a,b).show()
zip(a,b,c).show()
</mx:creationComplete>
</mx:Application>

And the output:

a: [1,2,3,4,5,6,7,8,9,10,11,12]
b: [a,b,c,d,e,f,g,h,i,j,k,l]
c: [q,r,s,t,u,v,w,x,y,z]
[]
[[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12]]
[[1,a],[2,b],[3,c],[4,d],[5,e],[6,f],[7,g],[8,h],[9,i],[10,j],[11,k],[12,l]]
[[1,a,q],[2,b,r],[3,c,s],[4,d,t],[5,e,u],[6,f,v],[7,g,w],[8,h,x],[9,i,y],[10,j,z]]

Composability

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.

Talk Back!

Have an opinion? Readers have already posted 7 comments about this weblog entry. Why not add yours?

RSS Feed

If you'd like to be notified whenever Bruce Eckel adds a new entry to his weblog, subscribe to his RSS feed.

About the Blogger

Bruce Eckel (www.BruceEckel.com) provides development assistance in Python with user interfaces in Flex. He is the author of Thinking in Java (Prentice-Hall, 1998, 2nd Edition, 2000, 3rd Edition, 2003, 4th Edition, 2005), the Hands-On Java Seminar CD ROM (available on the Web site), Thinking in C++ (PH 1995; 2nd edition 2000, Volume 2 with Chuck Allison, 2003), C++ Inside & Out (Osborne/McGraw-Hill 1993), among others. He's given hundreds of presentations throughout the world, published over 150 articles in numerous magazines, was a founding member of the ANSI/ISO C++ committee and speaks regularly at conferences.

This weblog entry is Copyright © 2008 Bruce Eckel. All rights reserved.

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2014 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use - Advertise with Us