Wednesday, July 8, 2009

looping through a class's static members

How do you loop through all static members of a class?

First of all, why would you want to do that? Well, let's say you're writing a game in which the player can move in the four classic compass directions:


public class Directions
{
public static const NORTH : String = "north";
public static const SOUTH : String = "south";
public static const EAST : String = "east";
public static const WEST : String = "west";
}


Somewhere in your game, you have a moveInDirection() method that accepts a direction. But you want to make sure it doesn't try to move the player in illegal directions, e.g. moveInDirection("up"); The IDE should prevent this, because if you type moveInDirection(Directions.UP) you'll get an error (because there's no UP constant).

However, I'd like to make moveInDirection a bit more idiot proof than having it rely on the IDE. Also, it's possible that directions may be chosen at run time. For instance, a player might type a direction in a textfield, as in a text-adventure game. What if the player types "up"?

So I'd like moveInDirection() to check if the player is trying to make a legal move...


function movieInDirection( direction : String ) : void
{
if ( isLegal( direction ) )
{
...
}
}


The question is, how should isLegal() work? The last thing I want to do is to use a bunch of conditionals or an Array...


function isLegal( direction : String ) : Boolean
{
var legal : Array = [ "north", "south", "east", "west" ];

for each ( var legalDirection : String in legal )
{
if ( direction == legalDirection ) return true;
}

return false;
}


That's a terrible solution, because it couples the class Directions to the Array in my isLegal function. Scaling will be dangerous. For instance, what if I add a new constant to Directions...

public static const INSIDE : String = "inside";

... but forget to add it to the Array?

var legal : Array = [ "north", "south", "east", "west" ]; //Oops! Forgot to add "inside"

A better solution is to loop through all the constants in Directions and check if the player's attempted direction exists. But how do you loop through all the static members of a class? After playing around with arcane solutions involving prototype chains, I settled on using the little-known describeType() function. It's in the flash.utils class, and you import it like this:

import flash.utils.describeType;

If you feed describleType() the Directions class -- trace( describeType( Directions) ) -- it outputs an XML description of the class:


<type name="Directions" base="Class" isDynamic="true" isFinal="true" isStatic="true">
<extendsClass type="Class"/>
<extendsClass type="Object"/>
<constant name="WEST" type="String"/>
<constant name="NORTH" type="String"/>
<constant name="EAST" type="String"/>
<constant name="SOUTH" type="String"/>
<accessor name="prototype" access="readonly" type="*" declaredBy="Class"/>
<factory type="Directions">
<extendsClass type="Enum"/>
<extendsClass type="Object"/>
</factory>
</type>


It's then a fairly easy matter to parse the xml, extract all the constants, and test them against the player's choice. I packaged the solution in a utility class:


package com.grumblebee.constants
{

import flash.utils.describeType;

public class Utilities
{
public static function isLegal( enumClass : Class, value : String ) : Boolean
{
var xml : XML = describeType( enumClass );
var xmlList : XMLList = xml.child( "constant" );
var enumName : String;

for each ( var child : XML in xmlList )
{
enumName = child.attribute( "name" ).toString();
if ( enumClass[ enumName ] == value ) return true;
}

return false;
}

}
}


I can't shake the feeling that there's a better way to do this. I'd love to lose the XML parsing. But this is the best I can come up with for the moment. One thing I could do is transform the player's string to upper-case and test it directly to see if there's such a constant in Directions:

if ( Directions[ playerString.toUpperCase() ] != undefined ) ...

But that only works if I stick to the convention of NORTH = "north". It fails if Directions looks like this:


public class Directions
{
public static const NORTH : String = "n";
public static const SOUTH : String = "s";
public static const EAST : String = "e";
public static const WEST : String = "w";
}


it would also fail in the case of...

public static const UNDER_THE_BRIDGE : String = "under the bridge";

At one point, I thought about adding the isLegal() function to Directions. You can get a class to refer to its own static constants this way:

trace( prototype.constructor[ "NORTH" ] ); //north

Sadly, though prototype.constructor allows you to access individual class variables and constants, for some reason, you can't loop through its members:

for each ( var s: String in prototype.constant ) trace( prototype.constant[ s ] ); //no output

It also doesn't work if you explicitly name the class:

trace( Directions[ "NORTH"] ); //north
for each ( var s : String in Directions ) trace( Directions[ s ] ); //no output.

So until something better comes along, I'm stuck with XML parsing.

6 comments:

  1. why not have a static dictionary instead?

    ReplyDelete
  2. Sure, I could do that, but unless I'm missing something, it complicates the interface:


    public class Directions
    {
    public static allDirections : Dictionary = [ ... ];
    }


    Now, I can no longer refer to Directions.NORTH. I have to refer to Directions.allDirections.NORTH.

    Or do you have a better solution?

    ReplyDelete
  3. well, I'd be willing to type a little extra to avoid some crazy XML parsing, I guess. That said, you could make a dummy lookup function called "get" so that when you're fetching a single item you can just call Directions.get() (plus then if you wanted to you could convert all input to uppercase or whatever else) but if you're enumerating through everything you can use allDirections directly (or, for real encapsulation and future-proofing etc in case you for some reason wanted to add some logic to it, make that an accessor too-- allDirections()-- and don't make your dictionary public).

    All of this might be me being too stuck in the old ways, but extracting class definitions dynamically as XML and then parsing it just makes me want to find the kids who are doing it and get them off my lawn.

    ReplyDelete
  4. It's an interesting tradeoff, anon. You're right that it took more time to write the XML-parsing routine than it would have if I'd just used a Dictionary. Also, parsing just seems clunky for something like this.

    On the other hand, when I have to choose between serving the original programmer (me) and the eventual user of the interface (maybe also me, but, if so, me in the future when I've forgotten about implementation details), I generally choose to serve the user.

    Programs are modified much more often than they are written.

    It's a standard in the AS community to use classes of static consts as enums. It's even built into the core library of the language, as in Event.COMPLETE. So it seems a bit perverse -- from a user-interface perspective -- to buck that trend.

    Even if I choose to buck it myself, I can't force others to buck it. The good thing about my solution is that I can apply it to other people's classes, as long as they use the standard "const" approach.

    It irks me a bit that Adobe only gives you an XML object for class introspection. Why can't I just use a for...each loop to search through all the consts? Under the hood, the xml must be built via some process that does what I want to do. But as far as I can tell, it's a low-level process that isn't available to developers.

    I have a sneaking suspicion that there's some voodoo I could do with prototypes, but it there is, it's eluding me.

    ReplyDelete
  5. That's fair. I'm pretty new to Actionscript so I don't know what's common. It's pretty lame that they don't have real enums since most modern languages do and they would make this problem trivial.

    ReplyDelete
  6. I agree that it's lame. Actionscript has come far as a language, but it still has a ways to go. The Enum thing really bothers me, because I LOVE Enums. It's possible I love them too much. For instance, this class goes into almost every project I work on:

    public class Common
    {
    public static const EMPTY_STRING : String = "";
    public static const NEWLINE : String = "\n";
    public static const TAB : String = "\t";
    public static const COMMA : String = ",";

    ...
    }

    It irks me to see if ( userName == "" ). I want to see if ( username == Common.EMPTY_STRING ).

    In addition to Enums, I really really want Actionscript to support overloading. I believe in passing all required setup data in through a class's constructor. I hate this sort of thing:

    var ufo : SpaceShip = new SpaceShip();
    ufo.baseColor = 0x666666;
    ufo.hasThrusters = true;
    ufo.thrustersColor = 0x0000CC;
    ufo.hasBlaster = false;
    ufo.render();

    What bothers me about that is there's no easy way to tell which of those properties are required to be set before calling render(). If required values must be set in the constructor, there's no ambiguity:

    var ufo = new SpaceShip(baseColor = 0x666666, hasThrusters = true, thrustersColor = 0x0000CC, hasBlaster = false);
    ufo.render();

    But that's a stupidly long-winded constructor. I want this:

    public function SpaceShip( baseColor : uint ) { ... }

    public function SpaceShip( baseColor : uint, hasThrusters : Boolean ) { ... }

    etc.

    Sure, you can use defaults...

    public function SpaceShip( baseColor : uint, hasThrusters : Boolean = false... ) { ... }

    ... but that gets overly complicated if you have many possible ways of constructing an object.

    While I'm on a rant, I also have come to really love languages like Objective C, in which you have to name parameters when you're calling a function. I don't like the C-based convention of...

    someFunction(1, false).

    What is the purpose of the 1? What is the purpose of the false? To know, you have to dig into documentation. You should be able to tell by just looking at the code.

    The one thing I DO like about the c-based convention is that it forces you -- if you care about readability -- to choose your function names carefully, so that they do the work of explaining the parameters:

    isValueInArray( 5, [1,2,3,4,5] );

    But that can make function names overly long. I would prefer...

    find( searchFor: 5, inArray: [1,2,3,4,5] );

    And if you combine that with overloading, you can also get variations like...

    find( searchFor: "cat" inString: "catatonic" );

    ReplyDelete