Enum & Sealed Classes

binary

Enum is an abbreviation for ‘enumerated’.  The overall concept is processing information where each value is one of a very limited set of values.  This is a look from the basics to the more advanced in how a dynamic language like python treats enum, through the advanced treatment available in kotlin.

TLDR; Just read the headings until you find sections of interest.

Ascii characters are one of 127 possible characters (although now we use larger character sets), and the original ‘integer’ values were 0,1,2, …. 65,535,  but what about when you have your own finite set of possibilities, like expressing location as ‘at home’, ‘at work’ or simply ‘elsewhere’? Enter, the Enum.

Background

With Digital Data, Everything is a number.

In digital computers, every value becomes a number.  Some values, for example a ‘speed’, or a length measurement, just are numbers already. To store the value as a number is obvious.  In fact at the simplest level, we have to types of data: integers and floats which are numbers.

Strings: All data can presented, and stored as a string.

What about all the things that are not normally numbers? Human language in written form represents all values as sequences of characters. By assigning a number to every possible character, any value we like can reduced to a string of characters, and those characters each stored as a series of numbers. We call this the string type.  We can store anything, regardless of whether the characters happen to be number or not, so we could even store our numbers as strings.  

On this basis there is no need to store values as the previously discussed ‘int’ or ‘float’, and every value, including numbers can be stored as a string. The string is effectively the common representation we could use for all types of data. 

In a sense, all types other than string are subsets of the possible strings. Integers are subsets where only numeric characters are allowed.  Simplistically, the syntax of float is as with an integer with the addition of a single “.” character. Both integer and float as strings are just subsets of all possible strings. In fact you could design a language where string is the only type.

Why not keep everything as a string?

There are advantages not simply treating all data as a string. This video provides a detailed answer and is worth watching if/when you have the time, and a shorter answer is that storing all data as strings reduces the data.

With the example of an integer converted to a string, every character will be a ‘digit’. A six character ‘ascii string (128 possible values per character ) has 128^6, or over 4 quadrillion possible values. If a six character string represents an integer, then there are only 10^6, or one million possible values. The subset of 6 characters strings that are integers is a relatively small subset. Each ‘integer’ string has special properties such as being valid for mathematical calculations. If a variable is simply ‘string’, then code would need to test if operations are valid which could be performed without needing the tests if it known the value is an integer. In simple terms, the less possible values a variable has, the less code required to deal with all possibilities. Storing numbers as strings, discards the information that the number of possible values is restricted and all follow specific rules. Code without that without that information requires more logic than code that can assume the rules are followed.

Further, the less possible values, the smaller the storage required to indicate which value is stored. For any given length of number, there also less possible alternatives than there are with strings. In the UTF-8 character set,  there are 1,112,064 possible values for each character.  In an integer, there are 10 possibilities for each character.  This allows for far more efficient storage of as an entry within the possible integers, than keeping them as string format which means keeping them as one of the far greater number of possible strings.  The less possible values, the more efficient the storage.

So far, integer, float and string, representations have been discussed, but may values have different restrictions. Reflecting the restriction possible values reduces required code, required storage and the thought required to understand the possibilities. ‘Enum’ or ‘enumerating the possible values’ is, where possible, the optimum solutions for data processing, storage and clarity.

Boolean: The simplest ‘Enum’.

If you are reading this, it can be assumed you understand Boolean, but reviewing the basics is still worthwhile.  Boolean is a type where the values are not characters and not numbers, and therefore fits the classic definition of an Enum type: data with a limited number of possible values that we can ‘enumerate’. For Boolean, there are just two possible values: false and true.

Anything where the choices can be ‘enumerated’, or each assigned to a numeric value, fits the pattern on an enum.  Think of any data you fill out on a form where the answer is one of a fixed set of choices, such as gender: male/female/not-saying, or Title: Mr/Mrs/Dr/Prof/Miss/Ms  or even agreement: strongly-agree/agree/unsure/disagree/strongly-disagree.

Each of the possible choices could be represented as a number, for example:

TITLE_MR = 1
TITLE_MRS = 2
TITLE_DR = 3
#  etc.......

The value for title could be kept in an integer.  In fact a language could do the same with Boolean, and simply have false=0 and true=1 would cover most requirements.  So technically, no Enum values are needed at all.  Just as we could not even bother with the constants and just remember that title of Mrs is number 2.  However, code that requires the reader rember each value is harder to read. Having enums which specifically declare what the possible values are, makes the code intent far clearer, and the code more easily scanned by a language or a reader for errors.

History

Enums have been added to Python 3.4 as described in PEP 435. It has also been backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4 on pypi.

Prior to the introduction of Enum in the language officially, code could use constants for the alternate values as described in background, or made use of a class to implement enums.   In fact libraries for Enum were sufficiently widely in use, that these libaries were the basis for Enum as added to the python language.

Enum in Python

There is a very simple format for declaring an enum:

>>> from enum import Enum


>>> Title = Enum("Title", "Mr Mrs Dr") # create type for title
>>> from enum import Enum # do import
>>> title= Title(2) #instance using value
>>> title
<Title.Mrs: 2>
>>> title2=Title["Dr"] # instance using name
>>> title2
<Title.Dr: 3>
>>> title.name # the attributes of 'title'
'Mrs'
>>> title.value
2
>>>

This example shows creating, and using, an Enum by specifying the names each alternative within the Enum.  In python, each alternative also has a value, and with this format, values for each alternative are assigned automatically, in a sequence starting with 1, so this case uses the values 1, 2 and 3.

Why start at one and not zero? Because in python the number zero can also be considered false, and the design is for every value by default to be true.

An alternative form allows choosing values as well as names. Note that although most of the examples shown use the values 0,2,5 to show it is possible, this should only be done in exceptional circumstances, such as for compatibility with legacy data. Normally, values for Python Enum should start at 1, and increment by 1.

>>> class Title(Enum):
    Mr=0
    Mrs = 2
    Dr = 5
>>> title = Title(0)
>>> title
<Title.Mr: 0>
>>> title2 = Title["Dr"]
>>> title2.name
'Dr'
>>> title2.value
5
>>>

Note: this does allow starting at zero if you wish, but the default behaviour is to start with the value 1, as 0 behaves as ‘false’, which can create confusion.
This alternative form can also work with automatic values:

>> from enum import Enum, auto
>>> class Title(Enum):
    Mr = auto()
    Mrs = auto() # using all automatic values
    Dr = auto()
#
>> title2 = Title.Dr # same value as Title["Dr"]
>>> title2
<Title.Dr: 3>
>>> class Title(Enum):
   Mr = auto()
   Mrs = 5 # choose value # can mix auto() with other values
   Dr = auto() # value will follow from previous chosen
#
>>> title2 = Title.Dr
>>> title2
<Title.Dr: 6>

In all these example, the value has been integer, but ‘value’ can be anything: a float, a string, None, True or False and even a mutable list or a user created class. Auto() still works mixed with any other values, but ignores any non-numeric so

Mr = auto()
Mrs = None # Could be True or even [1,2,3]
Dr = auto()
#  will have values 1, None, 2

This has been a simple review of enums in python, and illustrated the general concept. See the official python documentation for more details. Note that some cases provided by python, are oriented to supporting implementations made before Enum was added to the language.

Python Enum Internals.

(note this section is not required for understanding enums, and is for those simply interested). Skip to ‘Enums; a closed class‘ below, for the outcome of internals)

Checking the type of ‘Enum’ reveals it is not a standard type. As per the code below, most ‘types’ (e.g. float, int, or the class ‘Fred’ in the example below) are a ‘class type‘, and when a ‘float’ or a ‘Fred’ object is created, it will have a type that comes from the class used to create it.

from enum import Enum
class Title(Enum):
    Mr=0
    Mrs = 2
    Dr = 5

class Fred:
    pass

print(type(float))
print(type(Fred))
print(type(Fred())
print(type(Enum))
print(type(Title))
print(type(Title.Mr))

# output follows
<class 'type'>
<class 'type'>
<class '__main__.Fred'>
<class 'enum.EnumMeta'>
<class 'enum.EnumMeta'>
<enum 'Title'>

The class of ‘Enum’ is an EnumMeta class from the enum module, and not a regular type. See this page for a discussion on python metaclasses.

There are two ways to use Enum to create an enum class, and neither does what a standard ‘type’ would do. The two mays are:

  • Instance an enum object with two string parameters
  • Declaring a class inheriting from Enum

Instancing an Enum object with two string parameters (as in the example above), returns another ‘EnumMeta’ object, which is itself a metaclass. The returned metaclass cannot be used to instance new objects, as all objects of the resulting type have already been created, in behaviour the same as when declaring a class inheriting from Enum.

Declaring a class inheriting from Enum, instead of the usual ‘type’ object for a class (as with Fred in the code above), also returns an ‘EnumMeta‘ object. Note, again from the code above, that ‘Title.Mr‘ is an ‘Title‘ object. In fact each of what would normally be class variables, have been re-assigned as ‘Title‘ objects with the value they were originally assigned as the ‘value‘ property of the ‘Title‘ object.

Python Enum Classes are ‘Closed’ Classes.

Also note that no new ‘Title‘ objects can be created.

In the above example the following 3 Title objects were created:

  • Title.Mr
  • Title.Mrs
  • Title.Dr

And once the ‘Enum’ class has been created, no new objects can be created, which means this Enum will always have just 3 objects, and these 3 objects are immutable. Each of the Title objects has a ‘name‘ and a ‘value‘ attribute, neither of which can be changed.

mrs = Title.Mrs
print(f"name: {mrs.name} value: {mrs.value}")
mrs.value = 7

## Output below

name: Mrs value: 2
---------------------------------------------------------------------------
AttributeError  (can't set attribute)

The ‘Title(<value>)’ cannot be used to instance new 'Title‘ objects, and instead will only retrieve one of the original 'Title' objects by value.

Title(1)
----------------
ValueError: 1 is not a valid Title

Title(2) is Title.Mrs
### Output
True

In fact, the main thing separating an Enum class from other classes, is that there will always be a fixed number of members of the class, providing a fixed set of alternate values.

# a class similar to the Title class, without being an enum.
class Title:
    def __init__(self, name, value):
        self.name = name
        self.value = value
        
    Mr = Title("Mr", 0)
    Mrs = Title("Mrs", 2)
    Dr = Title("Dr", 5)

Working With Python Enum Classes.

In Summary:

  • each value of the Enum is an immutable object with two properties
    • name
    • value
  • no new Enum values can be added to the after the class is originally created
  • to compare Enum objects the ‘is‘ operation should be used as objects are unique
  • <class name>( <value> ) (e.g. Title(2) is Title.Mrs)
    • will retrieve the Enum object with the matching value if one exists
    • will raise a ValueError exception if no object has that value
  • <class name>[ <string> ] (e.g. Title[“Mrs”] is Title.Mrs)
    • will retrieve the Enum object with the matching name if one exists
    • will raise a ValueError exception if no object has that name

Enum in Kotlin

enum class Title{
    Mr,
    Mrs(),  // can add brackets after name, but does nothing in this case
    Dr
}
val title = Title.Mrs
println("$title  ${title.ordinal} ${title.name}")

This is almost an exact match for the python ‘auto()’ allocation of values. The differences are about ordinal vs value. Firstly, the name is different (ordinal vs value), and ‘ordinal’ starts from the more usual zero, not one, kotlin does not need to avoid the clash with python ‘Boolean safety’.

But the biggest difference is that ordinal is fixed. Like the indexes of an array, it must be the sequence of integers from zero … eg 0,1,2 …. It cannot be of another type, nor any other integers. Almost in every case, with both languages, enums will use the default ‘increment by 1’, but there are cases, such as when using values originally generated with the other language, where manual control of values helps.

Each value (Mr, Mrs, Dr) is an instance of the class Title, just as the values are objects in Python. Instancing an object normally would require something like ‘Mr = Title()', where parameters to ‘Title‘ are passed inside the parenthesis. If the base class requires parameters, theses must appear after the name of the enum element, but where there are no parameters, the the parenthesis can be omitted. As Title objects in this example take no parameters, and the parameters are not needed.

However, with kotlin as the values are all also instances of a custom class, it is possible to define this class with other properties, which allows replicating the exact Python class including values.

enum class Title(val value:Int){
    Mr(0),
    Mrs(2), // now the brackets are useful
    Dr(5)
}
println(Title.Mrs.value)
// Output  (note: matches the Python implemenation)
2

Kotlin enum classes provide for mapping ‘ordinals’ or value names to the objects of the enum through the <class>.valueOf(<string>) and class.values() companion object functions. This provide a full equivalent for value in python:

>>> class Title(Enum):
   Mr=0
   Mrs = 2
   Dr = 5
>>> title = Title(0)
>>> title
<Title.Mr: 0>
>>> title == Title.valueOf("Mr")
true

Unlike Python, this does not allow ‘value’ (ordinal) to vary in type between class members either, but as in Python there is no practical use to assigning values of different types to the values of different choices. Technically it is possible to assign a value type of ‘Any‘ or even to do strange things like each object has a body defined if that was really desired (although it is difficult to see the use):

// note this code valid but NOT RECOMMEDED
enum class Title{
    Mr() {val value = 2},  // legal for object to have a body- but useless
    Mrs { val value = "abc" } // again () brackets after name are optional
}

Although properties could be added in this way, given that each object is a single instance of Class ‘Title', would require reflection or something to access, and would have a fixed value anyway, this is more academic. The best use of the ‘body’ use of properties defined inside the body of the object, and rather pointless since each is and object and not a class of objects.

Generally, the Kotlin Enum is an effective replacement for the Python Enum. Where it gets interesting, is that in Kotlin it does not stop there, there is also sealed classes, which can be used as ‘extended Enums’.

Kotlin Enum Classes are ‘Closed’ Classes.

Again, note that no new ‘Title‘ objects can be created.

In the above example the following 3 Title objects were created:

  • Title.Mr
  • Title.Mrs
  • Title.Dr

And once the ‘enum’ class has been created, no new objects can be created, which means this enum will always have just 3 objects, and these 3 objects are immutable. Each of the Title objects has a ‘name‘ and an ‘ordinal’ attribute, neither of which can be changed.

val mrs = Title.Mrs
println("name: ${mrs.name} value: ${mrs.ordinal}")
mrs.name = "Ms"

## Output below

name: Mrs value: 1
---------------------------------------------------------------------------
Val cannot be reassigned

The ‘Title()’ cannot be used to instance new 'Title‘ objects, and instead will report

Cannot access ”__init__: it is private in ‘Title’
Enum types cannot be instantiated

Special when Support.

val mrs = Title.Mrs
when(mrs){
// autocomplete with give the message:
// 'when expression with enum should be exaustive, and 'Mr' and Mrs' branches 
//  or else branch
}

In fact, the main thing separating an enum class from other classes, is that there will always be a fixed number of members of the class, providing a fixed set of alternate values, providing a special form of when statement.

// a class similar to the Title class, without being an enum.
data class Title(private val name :String, private val value:Int)
// simulating a closed list - but still the class is not closed so no
// 'when' support
object Titles{       
    val Mr = Title("Mr", 0)
    val Mrs = Title("Mrs", 2)
    val Dr = Title("Dr", 5)
}
Titles.Mrs

Working With Kotlin Enum Classes.

In Summary:

  • each value of the Enum is an immutable object with two properties (plus any number of additional properties)
    • name
    • ordinal
  • no new Enum values can be added to the after the class is originally created
  • to compare Enum objects the ‘===’ operation should be used as objects are unique
  • <class name>.values()[ <ordinal> ] (e.g. Title.values()[1] === Title.Mrs)
    • will retrieve the Enum object with the matching ordinal if one exists
    • will raise an ArrayIndexOutOfBoundsException if no object has that ordinal
    • Note: <class name>.values().getOrNull( <ordinal>) to avoid possible exception
  • <class name>.valueOf( <string>) (e.g. Title.valueOf(“Mrs”) === Title.Mrs)
    • will retrieve the Enum object with the matching name if one exists
    • will raise a IllegalArgumentException if no object has that name

Beyond Enum: Sealed Classes

The Limitation of Enums.

So far our example has been ‘Title‘, and just keeping it brief dictates that the examples have not even attempted to cover all possibilities. While any reasonable list would of course include Ms and Miss, what about Dame, Madam, Duchess, Duke or Sir?  In Italy Engineer is actually a title so we need to add Eng.  Could we ever be sure to cover every title a person might consider as their appropriate title?  One solution is to simply have people tick “other”.  A problem is that simply recording “other” is almost the same as having no data.  While adding a “please specify” may feel better, without actually recording what is specified, this is does not solve the problem.

Neither a python Enum,  nor a kotlin Enum, can handle the “other” case.  Every value in an enum is effectively mapped to a fixed object, with fixed values. Why not add a “specify” property?  Because it does not achieve the goal.  If we have four possible values “Mr/Mrs/Dr/Other” these could be mapped to 1,2,3,4 for storage.  Each choice is just one object.  Every time “Other” is recorded it is exactly the same, including any properties of other, so there is only one value “specify”.  To have additional values for specify we need a new entries in our list in the Enum defintition.  Something like “Mr/Mrs/Dr/Other1(“Miss”)/Other2(“Ms”) ….. in other words we still need to state every possible value of all properties at the time the Enum is declared.

Sealed Classes: Building on Enums.

So what are enums? In ‘C’ and ‘Pascal’ and other older languages, an enum is a fixed set of integer values, with each value having a compile time assigned name. Python and Kotlin wrap these integer values together with the name, into an object, so that enum become a fixed set of instances of objects of a specific class.

What is common in all implementations, is that an enum represents a choice from a fixed set of choices. New choices cannot be added dynamically.

Sealed classes simply takes a further step, moving from a fixed set of instances of an object of a specific class, to a fixed set of subclasses of a given class.

This a custom sub-class or type (and a type is really a class) for each possible ‘enumerated’ value. 

Enums on Steriods?

To represent the data from the enum examples and solve the ‘limitation’ problem requires, as subclasses of the Title class:

  • a Mr class
  • a Mrs class
  • a Dr class
  • and an ‘Other’ class.  

Unlike an enum, as a ‘sealed class’ the while the choices are constrain to four specific classes, then no longer need be constrained to four specific objects. Any individual class could have any number of objects.

sealed class Title {  // no class parameter required
    object Mr   // each entry can class or a singletion object of that class
    object Mrs  // having a singleton object is just reproducing an Enum
    object Dr   // 
    class Other(val text:String)  // introduction of an actual class !
}

Code can be written similarly to when using enum, and the Kotlin ‘when’ feature applies just like enums.

The limitation is overcome, as now ‘other’ Titles can now be represented. Data can be represented by sealed classes that cannot be represent as an enum. Sealed classes can be seen as an ‘enum on steroids’, or ‘enum that can handle more complex data’.

Steroids are not always a good thing!

While there is still a constraint to four subclasses of title, the original concept of the enum, where the number of choices itself is constrained, is no longer present. The key concept is that many major software advanced result from restricting options, not from adding more options. Think loops, which give less options than goto, and immutable variables that are simply ‘variables’ than can no longer ‘vary’.

If we have made any advances in software since 1945, it is almost entirely in what not to do.

“Uncle” Bob Martin – “The Future of Programming (57:45)

The whole point of Enums that they elegantly reduce the number of possiblities. Sealed classes do not provide that same level of reduction. Sealed classes can provide elegant solutions, just not elegant solutions to problems that can be solved with ‘enum’.

There will be a future page with more on problems best solved by sealed classes, which most certainly are a huge step forward from the use of ‘Any’, but are more ‘something like enum for the times an actual enum cannot be used’ than ‘a better enum’.

Note that while an enum can be serialised in JSON as a single value, and the relevant enum choice instanced back from a single value (e.g. using Title(2), or Title[“Mrs”] in Python), no equivalent operation is possible with the sealed class example, as there are an infinite number of possible objects and type cannot be easily and generically be inferred unless also explicitly serialised.

Some general rules for sealed classes are:

  • objects (singletons), should be reserved for exceptional cases
  • sealed classes are better thought of as a union of object forms than as an enum

You could take the above example and by adding ‘value’ bring things back to an enum with one case that breaks the rules of enum.

sealed class Title(val value:Int) {  // value to match previous enum examples- not needed
    object mr: Title(0)   // each object or class based on sealed class
    object mrs: Title(1)  // which gives a common base class
    object Dr: Title(2)   // entries matching the enum patter can be class or object
    // but entries making use of the power of sealed classes must be a class
    class Other(val text:String): Title(5)
}

Now their are simpler solutions for serialising and the underlying concept of enum has reintroduced, but none of this is inherent in sealed classes the way the fixed restrictions are with ‘enum’.

Conclusion.

Use of enum allows reducing values to the minimum, following in the principle that following the most constrained set of rules that allows coding a solution is the path of improvement. Enum should be used wherever it can. There are some cases where enum cannot be used, but the less constraining sealed classes can be used.

If there are questions, add a comment and I will attempt to address them.

2 thoughts on “Enum & Sealed Classes”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s