6 Sep 2012

Enums

Enums are the next problem in Java to CoffeeScript conversion. Let's see, how to write a enum in CoffeeScript.
The first way is quite simple and obvious. If the only thing that we want from enum is providing a set of values that we can compare with each other, we can write it as follows:
Seasons = {WINTER : 0, SPRING : 1, SUMMER : 2, AUTUMN : 3}
Simple, isn't it? We just declare a new Object, that contains several properties - our enum constants. Now we can use it:
commentSeason = (season) ->
  switch season
    when Seasons.WINTER
      console.log "The coldest season"
    when Seasons.SUMMER
      console.log "The hottest season"
    else
      console.log "The rainy season"

commentSeason Seasons.WINTER
commentSeason Seasons.SUMMER
commentSeason Seasons.SPRING
console.log "Are spring and autumn the same? %s",
  if (Seasons.SPRING == Seasons.AUTUMN) then "Yes" else "No"
The output is:
The coldest season  
The hottest season  
The rainy season  
Are spring and autumn the same? No  
Well, this variant is incredibly simple and will be enough for most of the situations when you decide to use enum in your code. But what should you do if you want to use some of these Java enum features like getting constant name, or finding constant by name, or using constructors for creating constants and adding methods to them? My solution is below.
Java enum constants are not numbers with names - they are instances of enum class they are declared in. And every enum is actually a class which you can add any number of additional methods and fields in and get all the constants in one array from. Quite comprehensive thing, to be honest.
That's why I tried to write a new class that will be parent for all other enums in CoffeeScript. Let's first look at the sample of its child:
class Seasons extends Enum
  @size: 0
  @_VALUES: {@WINTER, @SPRING, @SUMMER, @AUTUMN}

  @WINTER: new (class extends Seasons
    comment: () ->
      console.log "%s is the coldest season.", @name()
  )(-8)

  @SPRING: new (class extends Seasons
    comment: () ->
      console.log "%s is rainy season.", @name()
  )(3)

  @SUMMER: new (class extends Seasons
    comment: () ->
      console.log "%s is the hottest season.", @name()
  )(15)

  @AUTUMN: new (class extends Seasons
    comment: () ->
      console.log "%s is rainy season.", @name()
  )(2)

  averageTemperature: undefined

  constructor: (@averageTemperature) ->
    super

  getTemperature: () ->
    if @averageTemperature > 0
      return @averageTemperature + " degrees above zero"
    else if @averageTemperature == 0
      return "zero"
    else
      return @averageTemperature + " degrees below zero"

  getAllValues: () ->
    return @getSuperclass().values()
First of all, give a special attention to lines 2 and 3. They are both absolutely necessary in child enum class. The @size field will hold the number of constants after their declaration and must be initialized by 0 before any other code. The @_VALUES field should contain the pre-declarations of enum constants. In fact, all this values are undefined before next lines, but they reserve names for your constants.
Then look at line 27, where the constructor is declared. You will not need it if you have no additional fields, but if you do, you should necessarily call super constructor (line 28). Below the constructor there are some more methods that every constant will have.
Coming back to top, look at the way how each constant is declared (e.g. lines 5-8) - it is static field of enum class, which is the instance of anonymous class inherited from Seasons. In our case this class has additional method comment, that prints out some text in stdout.
Now we have very powerful enum Seasons and can do a lot of things with it. We can get name and ordinal of each constant:
season1 = Seasons.WINTER
console.log "First season info: name: %s, ordinal: %s, t_avr: %s",
  season1.name(), season1.ordinal(), season1.getTemperature()
season1.comment()
season2 = Seasons.SUMMER
console.log "First season info: name: %s, ordinal: %s, t_avr: %s",
  season2.name(), season2.ordinal(), season2.getTemperature()
season2.comment()
The output is:
First season info: name: WINTER, ordinal: 0, t_avr: -8 degrees below zero  
WINTER is the coldest season.  
First season info: name: SUMMER, ordinal: 2, t_avr: 15 degrees above zero  
SUMMER is the hottest season.
Then, we can get all the values from enum class or even from one of its constants and get constant by name:
console.log "All values from enum class: %s", Seasons.values()
console.log "All values from enum const: %s", season1.getAllValues()
console.log "Enum const from name \"SPRING\": %s", Seasons.valueOf("SPRING")
The output is:
All values from enum class: WINTER(0),SPRING(1),SUMMER(2),AUTUMN(3)  
All values from enum const: WINTER(0),SPRING(1),SUMMER(2),AUTUMN(3)  
Enum const from name "SPRING": SPRING(1)
We can also compare constants and use them in switch:
console.log "WINTER equals SUMMER? %s",
  if season1 == season2 then "Yes" else "No"
console.log "WINTER less than SUMMER? %s",
  if season1.compareTo(season2) < 0 then "Yes" else "No"

switch season1
  when Seasons.WINTER
    console.log "Winter is passed to switch"
  when Seasons.SUMMER
    console.log "Summer is passed to switch"
  else
    console.log "Some other season is passed to switch"
The output is:
WINTER equals SUMMER? No  
WINTER less than SUMMER? Yes  
Winter is passed to switch  
I'm not sure but these seem to be the basic usages of enums even in Java.
The only thing that I have not told about yet - the Enum class itself. It's source code is the following:
class Enum
  @_size: 0
  @_VALUES: {}

  @values: () ->
    values = new Array()
    for value in Object.keys(@_VALUES)
      values.push @_VALUES[value]
    return values

  @valueOf: (name) ->
    return @_VALUES[name]

  _name: undefined
  _ordinal: undefined

  constructor: () ->
    Class = @getSuperclass()
    @_name = Object.keys(Class._VALUES)[Class._size]
    @_ordinal = Class._size
    Class._size += 1
    Class._VALUES[@_name] = this

  name: () ->
    return @_name

  ordinal: () ->
    return @_ordinal

  compareTo: (other) ->
    return @_ordinal - other._ordinal

  equals: (other) ->
    return this == other

  toString: () ->
    return @_name + "(" + @_ordinal + ")"

  getClass: () ->
    return this.constructor

  getSuperclass: () ->
    return @getClass().__super__.constructor

exports.Enum = Enum
I suppose that it doesn't need much explanations, but I do want to comment out the constructor (lines 17-22), as it is very important. In fact, the constructor is called in anonymous class. That is why for each constant getSuperclass() will return enum class and getClass() - one of anonymous classes. Thus Class variable will be Seasons in our example.
What we want to do is to store in Seasons._VALUES references to our constants after all these constants are declared. At the same time, we need some place where we can get the name of each constant in its constructor from. That is why in Seasons class we pre-declared constants (look at line 3 of Seasons source code). Note that you should declare them in the same order as you did in Class._VALUES, otherwise you will get wrong names. Now, in line 19 we get all the keys of Class._VALUES (i.e. an array with 'WINTER', 'SPRING' etc) and get the key at position equal to its ordinal. At the end of constructor we redefine Class._VALUES[@_name]: it was equal to undefined before and now get reference to this instance.
Well, I thing that is all for now. I'm not sure that my solution is the most optimal and elegant. I'll be glad to know your opinion and to discuss any other variants of creating Java-like enums in CoffeeScript.

To be continued...

No comments:

Post a Comment