Refactoring to Fantom

This post in an introduction to Fantom via the refactoring of Java code.

TL; DR: Fantom is basically a better Java with a very similar syntax, but with lots of little simplifications and some powerful features such as functions.

La citerne-basilique

Let’s say you want to implement FooBarQix.

For those not in the know, FooBarQix is a variant of FizzBuzz using “Foo” in place of “Fizz” and “Bar” in place of “Buzz.” One difference is that “Qix” plays the same role as “Foo” and “Bar” for number 7. Another difference is that digits 3, 5 and 7 also cause the results to contain instances of those words. FooBarQix receives a minor celebrity status in France, in November 2011 as part of a competition related to the upcoming Devoxx conference in Paris.

 

Here are a sample output (the C-style comments are not part of the output):

1
2
FooFoo
4
BarBar
Foo
QixQix
8
Foo
Bar
11
Foo
Foo // for 13
Qix
FooBarBar // for 15
...
FooQix // for 21
...
FooFooFoo // for 33
...
FooBar // for 51
Bar
BarFoo // for 53
... // up to 100

Obviously, there are many different possible implementations. Here is one in Java:

public class FooBarQix {
	public static void main(String[] args) {
		System.out.println(new FooBarQix().asTextUpTo(100));
	}

	public String asTextUpTo(int topNumber) {
		String str = "";
		for (int i = 1; i < = topNumber; i++) { 			str += toText(i) + "\n"; 		} 		return str; 	} 	public static final String[] TEXT = new String[] { "", "", "", "Foo", "", 			"Bar", "", "Qix", "", "" }; 	public String toText(int number) { 		StringBuffer result = new StringBuffer(); 		addTextForDivisible(number, 3, result); 		addTextForDivisible(number, 5, result); 		addTextForDivisible(number, 7, result); 		addTextForDigits(number, result); 		return result.length() > 0 ? result.toString() : Integer
				.toString(number);
	}

	private void addTextForDivisible(int number, int digit, StringBuffer str) {
		if (number % digit == 0) {
			str.append(TEXT[digit]);
		}
	}

	private void addTextForDigits(int numberToConvert, StringBuffer str) {
		String asString = Integer.toString(numberToConvert);
		for (int i = 0; i < asString.length(); i++) {
			int digit = Integer.parseInt(asString.substring(i, i + 1));
			str.append(TEXT[digit]);
		}
	}
}

I am going to guide you through a refactoring of this from Java to Fantom.

Direct translation into Fantom code

At this stage, there are only minor differences:

  • System.out.println() => Env.cur.out().printLine
  • ‘=’ => ‘:=’ for field initialization
  • void => Void
  • String => Str
  • int => Int
  • new FooBarQix() => FooBarQix()
  • new String[] {…} => Str[…]
  • .toString() => .toStr()
  • StringBuffer => StrBuf
  • Integer.toString(number) => number.toStr()
  • string.substring(i, i+1) => string[i..i]
  • static final => const

Which leads us to the following Fantom class:

public class FooBarQix {
  public static Void main(Str[] args) {
    Env.cur.out(FooBarQix().asTextUpTo(100));
  }

  public Str asTextUpTo(Int topNumber) {
    Str str := "";
    for (Int i := 1; i < = topNumber; i++) {       str += toText(i) + "\n";     }     return str;   }   public const Str[] TEXT := Str["", "", "", "Foo", "",       "Bar", "", "Qix", "", "" ];   public Str toText(Int number) {     StrBuf result := StrBuf();     addTextForDivisible(number, 3, result);     addTextForDivisible(number, 5, result);     addTextForDivisible(number, 7, result);     addTextForDigits(number, result);     return result.size() > 0 ? result.toStr() : number.toStr();
  }

  private Void addTextForDivisible(Int number, Int digit, StrBuf str) {
    if (number % digit == 0) {
      str.add(TEXT[digit]);
    }
  }

  private Void addTextForDigits(Int numberToConvert, StrBuf str) {
    Str asString := numberToConvert.toStr();
    for (Int i := 0; i < asString.size(); i++) {
      Int digit := Int.fromStr(asString[i..i]);
      str.add(TEXT[digit]);
    }
  }
}

A couple of notes:

  • Unlike Java, Fantom is a pure object-oriented language, so all Java ints have been converted into normal Fantom objects
  • Str[…] might look like a Java-style array, but it is actually an instance of Fantom’s List; a variety of methods is therefore available on its instances
  • void is spelled Void in Fantom, and is an actual Class, although it has no methods available

Syntactic sugar

Lots of things are redundant in the Java syntax. Here is how they can be simplified in Fantom.

  • Type inference: it is no longer necessary to declare the type of a variable when it is defined with a typed value
  • Class and method protection scope defaults to public
  • Env.cur.out().printLine is commonly just written echo()
  • semi-colons at the end of the lines are optional
  • pair of parenthesis on method calls are optional (not on constructor calls, though)
  • List offers a convenient API that allows the chaining of calls, making the construction much clearer

Our code now looks like this:

class FooBarQix {
  static Void main(Str[] args) {
    echo(FooBarQix().asTextUpTo(100))
  }

  Str asTextUpTo(Int topNumber) {
    str := ""
    for (i := 1; i < = topNumber; i++) {       str += toText(i) + "\n"     }     return str   }   const Str[] TEXT := Str[,].fill("", 10).set(3, "Foo").set(5, "Bar").set(7, "Qix")   Str toText(Int number) {     result := StrBuf()     addTextForDivisible(number, 3, result)     addTextForDivisible(number, 5, result)     addTextForDivisible(number, 7, result)     addTextForDigits(number, result)     return result.size > 0 ? result.toStr : number.toStr
  }

  private Void addTextForDivisible(Int number, Int digit, StrBuf str) {
    if (number % digit == 0) {
      str.add(TEXT[digit])
    }
  }

  private Void addTextForDigits(Int numberToConvert, StrBuf str) {
    asString := numberToConvert.toStr
    for (Int i := 0; i < asString.size; i++) {
      Int digit := Int.fromStr(asString[i..i])
      str.add(TEXT[digit])
    }
  }
}

Functions as first class objects

One of the main differences with Java is that functions are available to pass around to other methods. This will be possible in Java 8 sometime in 2013. This has been available on the JVM with Fantom since 2005.
One common way of using a function is via the each() and map() methods, available on collections, strings, and others.

We’ll also take this opportunity to replace the list of values by a map. We could have done that in Java as well. However, this construct is a lot more common in a functional language (that is, one where functions are first-class). There is also a nice Map.get() that can take a default value.

class FooBarQix {
  static Void main(Str[] args) {
    echo(FooBarQix().asTextUpTo(100))
  }

  Str asTextUpTo(Int topNumber) {
    return (1..100).map |Int n| { toText(n) }.join("\n")
  }

  const Map TEXT := [3:"Foo", 5:"Bar", 7:"Qix"]

  Str toText(Int number) {
    result := StrBuf()
    addTextForDivisible(number, 3, result)
    addTextForDivisible(number, 5, result)
    addTextForDivisible(number, 7, result)
    addTextForDigits(number, result)

    return result.size > 0 ? result.toStr : number.toStr
  }

  private Void addTextForDivisible(Int number, Int digit, StrBuf str) {
    if (number % digit == 0) {
      str.add(TEXT[digit])
    }
  }

  private Void addTextForDigits(Int numberToConvert, StrBuf str) {
    numberToConvert.toStr.each |c| { str.add(TEXT.get(c.fromDigit, "")) }
  }
}

Completing the work

A couple of things still bother me: the 3, 5, and 7 digits are repeated in two places. The other thing is that we carry around a string buffer, which is not very elegant.

To remove the string buffer, we’ll get each method to return a computed partial string and concatenate the results.

As for the redundant digits, we’ll use the ones already defined in the map of values… and functions will be put to good and make it really easy to transform those values into strings.

As a additional syntactic sugar, we can omit the generic part of a function signature (when passed to map()) and use “it” as a placeholder for the parameter — or even nothing when there are no ambiguities.

Here is the final result:

class FooBarQix {
  static Void main(Str[] args) {
    echo(FooBarQix().asTextUpTo(100))
  }

  Str asTextUpTo(Int topNumber) {
    return (1..100).map { toText(it) }.join("\n")
  }

  const Map TEXT := [3:"Foo", 5:"Bar", 7:"Qix"]

  Str toText(Int number) {
    Str result := addTextForDivisibles(number) + addTextForDigits(number)

    return result.size > 0 ? result : number.toStr
  }

  private Str addTextForDivisibles(Int number) {
    return TEXT.keys.map |Int digit -> Str?| { number % digit == 0 ? TEXT[digit] : "" }.join
  }

  private Str addTextForDigits(Int numberToConvert) {
    return numberToConvert.toStr.chars.map { TEXT.get(it.fromDigit, "") }.join
  }
}

To be sure, it is possible to write Java code that gets close to that. This means using additional libraries such as Guava and using kludges such as inner classes in place of functions. However, this is clearly not natural in Java. Maybe that will change with Java 8.

Final thoughts

This post has not addressed a number of features offered by Fantom, such as portability (Fantom code can be run both on the .NET CLR and on the JVM), mixins, modules (“pods”), and null-safety. However, I hope to have made clear that Fantom is an intriguing language with a pragmatic syntax. This puts the newer kids on the block such as Kotlin and Ceylon really in an interesting light.

About Eric Lefevre-Ardant

Independent technical consultant.
This entry was posted in java. Bookmark the permalink.

1 Response to Refactoring to Fantom

  1. Tom says:

    Interesting introduction to Fantom, thx.

Comments are closed.