4
\$\begingroup\$

I'm very much used to enumerations a la Java, where each value in an enum is a distinct object. You can call functions on these objects, and they're generally treated as most normal objects.

Except ActionScript 3 doesn't have enumerations. So... I had to create my own.

My hard requirements:

  • Compile time type-safety checking
  • Target a single value via EnumClass.VALUE or similar constant notation
  • It should be possible to call functions on enumeration values
  • Serializing and deserializing an enumeration value should be possible without too much work
  • Getting all values in an enumeration should be easy

It'd be nice if I didn't need a whole lot of boilerplate to define an enumeration. If I must write boilerplate, then I'd prefer to write such boilerplate once - adding values should be easy. Same goes for removing values.

With that in mind, I have managed to create the following...

Base class

package {
    import flash.utils.describeType;
    import flash.utils.getDefinitionByName;
    import flash.utils.getQualifiedClassName;
    import flash.utils.getQualifiedSuperclassName;

    /**
     * Base class for Enumerations. 
     * @author Pimgd
     */
    public class Enum {
        private var value:uint;
        private var id:String;

        public function Enum(val:uint, ID:String) {
            value = val;
            id = ID;
        }

        private static function validateClassIsSubclassOf(classToCheck:Class, baseClass:Class):Boolean {
            //http://stackoverflow.com/a/6681302/540837
            var qn:String = getQualifiedClassName(classToCheck);
            var en:String = getQualifiedClassName(baseClass);
            if (qn != en) {
                qn = getQualifiedSuperclassName(classToCheck);//fix case where class isn't constructed yet?
            }
            while(qn != "Object") {
                if (qn == en) return true;
                qn = getQualifiedSuperclassName(getDefinitionByName(qn));
            }
            return false;
        }

        public static function loadValuesForEnum(enumClass:Class):Array {
            if (!validateClassIsSubclassOf(enumClass, Enum)) {
                ErrorManager.showDevelopmentError("loadValuesForEnum(" + getQualifiedClassName(enumClass) + ") failed - provided class is not a subclass of Enum!");
                return new Array();
            }

            var description:XML = describeType(enumClass);
            var arr:Array = new Array();
            var id:uint = 0;
            for each ( var constant:XML in description.constant) 
            {
                var constantName:String = [email protected]();
                if (constantName == constantName.toUpperCase()) {
                    arr[constantName] = new enumClass(id++, constantName);
                }
            }

            return arr;
        }

        public function getValue():uint {
            return value;
        }

        public function getID():String {
            return id;
        }

    }

}

Severity Enum - a simple marker enumeration

package {
    /**
     * The Severity enumeration is used to determine the severity of an Issue.
     * There are three levels/categories of severity:
     *   - SEVERE, for instances in which the issue has to be fixed, or the Dialogue will cause SDT to crash or otherwise experience uncaught exceptions;
     *   - MAJOR, for instances in which the Dialogue does not display as intended.
     *      Since the DialogueChecker does not have the ability to read the intention of the author of a dialogue, the DialogueChecker assumes that:
     *          - All lines in a dialogue that are not commented out were meant to be played. (Unused lines are bad)
     *          - All content in a line of a dialogue was meant to be displayed. (Failed substitutes are bad)
     *          - All lines in a dialogue were meant to function correctly within SDT. (SDT should not fail to parse any lines)
     *      In the event that the application of these assumptions to a dialogue fails,
     *      the cause of the failure will be labeled an issue of MAJOR severity, unless the cause is already labelled as a SEVERE issue.
     *   - MINOR, for instances in which the issue does not entail an error or meets the requirements for SEVERE or MAJOR severity.
     *      Issues with a MINOR severity tend to entail grammatical mistakes, unnecessary sections of a dialogue or line, or syntactic styling of dialogue lines.
     * These three levels can each be seen as MUST (Severe), SHOULD (Major) and COULD (Minor) - "be fixed".
     * In case the DialogueChecker is unable to parse a dialogue or sections of a Dialogue, issues are automatically treated as SEVERE, due to the possibility of the affected sections to contain SEVERE issues.
     * @author Pimgd
     */
    public class Severity extends Enum {
        private static const enums:Array = Enum.loadValuesForEnum(Severity);
        public static const SEVERE:Severity = enums["SEVERE"];
        public static const MAJOR:Severity = enums["MAJOR"];
        public static const MINOR:Severity = enums["MINOR"];

        public function Severity(val:uint, ID:String) {
            super(val, ID);
        }

        public static function getAllEnums():Array {
            var tempArray:Array = new Array();
            for each (var severity:Severity in enums) {
                tempArray[severity.getValue()] = severity;
            }
            return tempArray;
        }

        public function getIDStandardCapitalization():String {
            var ownID:String = getID();
            ownID = ownID.toLowerCase();
            ownID = ownID.charAt(0).toUpperCase() + ownID.substr(1);
            return ownID;
        }
    }

}

Mood enum - a more complex enumeration with various operations to get at the values

package {
    /**
     * Represents a mood that a character can have in SDT.
     * Moods affect how a character behaves. This has been extended to dialogue functions as well.
     * Dialogue writers can check for moods to allow a character to act differently on different situations.
     * This enumeration represents all the possible moods that SDT has.
     * @author Pimgd
     */
    public class Mood extends Enum {
        private static const enums:Array = Enum.loadValuesForEnum(Mood);
        public static const NORMAL:Mood = enums["NORMAL"];
        public static const ANGRY:Mood = enums["ANGRY"];
        public static const HAPPY:Mood = enums["HAPPY"];
        public static const AHEGAO:Mood = enums["AHEGAO"];//NSFW don't google

        public function Mood(val:uint, ID:String) {
            super(val, ID);
        }

        public static function fromString(id:String):Mood {
            return Mood(enums[id]);
        }

        public static function hasEnum(id:String):Boolean {
            return fromString(id) != null;
        }

        public static function fromMoodAttributeValue(value:String):Mood {
            if (value == null) {
                return null;
            }

            if (StringFunctions.stringStartsWith(value, "\"") && StringFunctions.stringEndsWith(value, "\"")) {
                value = value.substring(1, value.length - 1);
            }
            var moods:Array = getAllEnums();
            for (var i:uint = 0; i < moods.length; i++) {
                if (moods[i].getIDStandardCapitalization() == value) {
                    return moods[i];
                }
            }
            return null;
        }

        public static function fromTrigger(trigger:Trigger):Mood {
            if (trigger != null && trigger.getType() == TriggerType.MOOD) {
                return fromString(trigger.getAction().split("_")[0]);
            }
            return null;
        }

        public static function getAllEnums():Array {
            var tempArray:Array = new Array();
            for each (var mood:Mood in enums) {
                tempArray[mood.getValue()] = mood;
            }
            return tempArray;
        }

        public function getIDStandardCapitalization():String {
            var ownID:String = getID();
            ownID = ownID.toLowerCase();
            ownID = ownID.charAt(0).toUpperCase() + ownID.substr(1);
            return ownID;
        }
    }

}

How this works is that, upon first usage of the class, the class itself is instantiated. The static variable enums is initialized, which uses reflection via describeType to get a list of fields that need to be filled with enum values. You can see this in Enum.loadValuesForEnum. When the call returns with a string-indexed array, it's able to set the enum values from this array.


Usage example from my code in a validator:

var currentMood:Mood = null;
var checkedFor:Mood = null;
var checkingAgainstMoodAttribute:Boolean = false;
var moodAttribute:LineAttribute = lineObject.getLineAttribute("\"mood\"");
if (moodAttribute != null) {
    checkedFor = Mood.fromMoodAttributeValue(moodAttribute.getValue() as String);
    if (checkedFor != null) { //else failed to identify mood value? handled in checkLineOldStyle
        currentMood = checkedFor;
        checkingAgainstMoodAttribute = true;
    }
}

var triggers:Array = lineObject.getTriggers();

for (var i:uint = 0, isize:uint = triggers.length; i < isize; i++) {
    var trigger:Trigger = triggers[i] as Trigger;
    var setMood:Mood = Mood.fromTrigger(trigger);
    if (setMood != null) {
        if (setMood == currentMood) {
            issueSnippet = "";
            if (checkingAgainstMoodAttribute) {
                issueMessage = "Line " + lineObject.getLineNumber() + " checks for " + checkedFor.getIDStandardCapitalization() + " mood, but then also sets the mood to " + setMood.getIDStandardCapitalization() + " in the line itself." + linebreak;
                issueMessage += "It's impossible (except if the user manually changed it in the brief period in between the check and the trigger) for the mood to change in between the check and the set, so this trigger could be removed." + linebreak;
            } else {
                issueMessage = "Line " + lineObject.getLineNumber() + " sets the mood to " + setMood.getIDStandardCapitalization() + " multiple times in a row." + linebreak;
                issueMessage += "Repeated setting of the mood to the same value has no result, and thus the trigger can be removed." + linebreak;
            }
            issueMessage += "Line " + lineObject.getLineNumber() + ": " + line + linebreak + linebreak;
            issue = new Issue(Severity.MINOR, lineObject, issueSnippet, issueMessage, "Syntax");
            addIssue(issue);
            issueMessage = "";
            checkingAgainstMoodAttribute = false;//flip flag to allow "repeated mood setting" issue to pop up
        }
        currentMood = setMood;
    }
}

I'd like to know if there's a better way to make enumerations or if there's an easier way to provide fromString, getAllEnums and hasEnum from the Enum class. I'm also interested in general improvements.

\$\endgroup\$
0

1 Answer 1

-1
\$\begingroup\$

The problem I see with that, is that nothing stops your users (as in, the users of your code) to create a new instance of your enum class, which might lead to horrible crashes ...

Severity.MAJOR == new Severity(0, "MAJOR"); // BOOM!

In the past I used a technique called static constructor for this kind of problem.

When your class is loaded, before the first instance is created, you can execute some code, than in your case can initialise and then freeze the class definition into enum values:

public class Test {
    {
        trace("initialise then freeze");
    }
    public function Test() {
        trace("create");
    }
}

The other "problem" I see is the boilerplate code: as you say, less is better and none is best. I think you can fix most of it by using dictionaries of pre-cached values.

Also, why would you return just the values from the getAllEnums method? the name suggest you're returning the enum instances, not the plain values.

I "might" be biased but I like this implementation of enums in AS3. Here you can see an example of how it's used. Add parameters to the constructor and you have rich enum values.

The goals of that implementation are close yours:

  • Type-safety
  • Java-like syntax
  • Prevent direct instantiation
  • Low effort to roll your enumerations

If only we had generics...

\$\endgroup\$
5
  • \$\begingroup\$ How would you create a static constructor? You've kinda shown an example on how to execute arbitrary code at class initialization but I'm not seeing the "constructor blocking" functionality. Same goes for "dictionaries of pre-cached values" - could you show me an example? \$\endgroup\$
    – Pimgd
    Sep 5, 2016 at 15:20
  • \$\begingroup\$ getAllEnums returns an array indexed by enum ordinal value, where the array values are the enum instances themselves. So yeah, I am returning all the enum instances, and you can iterate over them with for ... in just fine. \$\endgroup\$
    – Pimgd
    Sep 5, 2016 at 15:21
  • \$\begingroup\$ Have a look at the two classes i linked, it's not far off from what you've done. Basically you do the initialisation (describeType+parsing etc) from the static constructor, then set some kind of flag the seals the class. check that flag in the constructor to throw an error on new instantiations \$\endgroup\$
    – alebianco
    Sep 5, 2016 at 15:43
  • \$\begingroup\$ yep, my bad on getAllEnums, I got confused :) \$\endgroup\$
    – alebianco
    Sep 5, 2016 at 15:44
  • \$\begingroup\$ Would you edit your answer with examples and improvements? More than half the answer doesn't apply at the moment. The first third is good, but the second third doesn't go in depth enough and the third... third is a link only answer. And "horrible crashes" don't even happen in your example, it will just return false? \$\endgroup\$
    – Pimgd
    Sep 5, 2016 at 17:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.