Method Overloading achieved with Abstracts in Haxe

Method Overloading in general

In this post I am going to introduce “method overloading” in the Haxe programming language. In general, method overloading means that a collection of functions with different type signatures, that share the same name. For example in Java, one would write the following code:

public void foo(String bar) { /* body ... */ }
public void foo(String[] bars) { /* body ... */ }

Then you can call either foo("my string") or foo(new String[]{"my", "string"}).

method overloading with haxe abstracts

Hacky ways to achieve method overloading in Haxe

Now, as the title says, this post is about Haxe. On Haxe there is no “official way” to declare method overloading (except for extern one can use the @:overload metadata). If one writes two methods with the same name the compiler will just refuse to let you pass. As a result, there are many codes try to rely on optional function arguments to achieve some kind of method overloading. For example, one would write:

public function foo(?bar:String, ?bars:Array<String>) {
  if(bar != null) bars = [bar];
  // then handle `bars`, blah blah blah
}

// so one can call either `foo('my string')` or `foo(['my', 'string'])`

But there is a problem, this way of coding one cannot guarantee that users only pass either a String or an array of String, but not both, compared to the Java case where the compiler will check that only either String or String[] can be passed to the function. For this Haxe case, one can write runtime code inside the function body to check that only one parameter is received, but that is both not runtime efficient and not type-safe which, as Haxers, we love.

Abstract steps in and save the world

Well, but that is not the end of the world! There is a hidden gem (besides macro) in Haxe called abstract and it will save us from this problem. The reason abstracts can save us is that we can declare implicit casts by using the @:from and @:to metadata. A piece of code worth a thousand words, let’s read some code:

@:forward
abstract Bar(Array<String>) from Array<String> to Array<String> {
  @:from public static inline function fromString(v:String):Bar
    return [v];
}

// then we declare the foo function again as:
public function foo(bar:Bar) { /* body ... */ }

And the magic is Done! Now, we can call the function foo with either a String or an array of String (but not both) and in the function body we only need to handle the string array inside the function body. There is no need to check for null as in the optional argument way and it is type-safe at compile time! (See it in action on Try Haxe)

Details explained as follow

First we declare an abstract. From the Haxe manual:

An abstract type is a type which is actually a different type at run-time.

To be more precise, an abstract is actually the “underlying type” at runtime. The “underlying type” is the type declared in the parenthesis. In this case it is Array<String>. In other words, at runtime Bar doesn’t really exists, whatever typed as Bar in compile time is just Array<String> at runtime.

Next, the from Array<String> to Array<String> simply tell the compiler to feel free casting Array<String> to Bar and Bar to Array<String>. They can interchange because, as mentioned before, Bar is just Array<String>. In some case we don’t want such compile time casts then we can just omit the from/to statements.

The coming line is where the method overloading magic happens, the @:from metadata declares a custom way to cast some type to Bar. Here we want to cast String to Bar so we put a single argument for this function and type it as String. Then in the body we wrap the String into an Array and return it. Since every @:from function must return the abstract type itself (Bar here), we explicitly declare the return type to be Bar, such that the Array<String> returned will be treated as Bar (as allowed by the from Array<String> statement mentioned above).

At last, we put a @:forward metadata, which is optional for our particular problem, so that the underlying methods and members will be forwarded. So you can access array functions and properties (e.g. push, pop, length, etc…) on values typed as Bar.

Here I demonstrated using abstracts to achieve method overloading for function arguments. Similarly, the return type of an function can be overloaded using similar techniques, by using the @:to metadata to implicitly cast the abstract to other types.

For more information, please refer to the Haxe manual and Haxe Code Book

8 thoughts on “Method Overloading achieved with Abstracts in Haxe

  1. Nice article! Correct me if I’m wrong, but there seems to be an error though in foo([‘my’, ‘string’]) which should be foo(null, [‘my’, ‘string’])

    1. Naming types and variables really depends on context and personal preference, so it is hard to tell a concrete answer.
      But in general, if the type particularly describes a list of bar names, I may name it as NamesOfBars. If it is just a generic String or Array abstract, I may name it as StringArray or StringOrArray, etc. It really depends.

  2. Nice trick. Note, however, that this still has runtime cost unlike “real” method overloading, when compiler statically dispatches a call to different generated methods. Also it’ll probably get tricky with more than one argument.

    1. Yes true, that’s a very good reminder. What I had in mind while writing is that it is quite common that overloaded functions are used for adding variations and options. Ultimately they will be forwarded to a “core” function that holds the actual implementation. In that case runtime cost is more or less the same as the approach described in this post. On the other hand, if the each of the overload has its own very different implementation, I think they should be assigned different names altogether.

  3. could haxe.ds.Either help in this case ?

    like bar:haxe.ds.Either<String,Array>

    I dont know if there is some logic behind this type, or if it’s only here to “carry” two types and nothing more

Leave a Reply

Your email address will not be published. Required fields are marked *