COMMUNITY

Macro for overloading via [ static extension or abstract ]

macro

(Jeff Ward) #1

I’m toying with a macro for method overloading support for static extensions.

First, let me say, I’m not advocating Haxe should support overloaded methods natively (there is an evolution proposal). And Haxe is already pretty flexible with function args via a OneOf abstract (though as an enum argument, it incurs runtime overhead and is not easy to use at runtime.)

But for some flavors of API, I simply prefer overloaded methods, as it takes up less space in my brain. Take, for example, JavaScript-like String.replace signatures. It’s so nice to have all three of these…

…without having to remember where they exist in the Haxe API. e.g. these three are:

  • StringTools.replace
  • EReg.replace
  • EReg.map

Ugh. It’s simply less cognitive load to say String.replace is going to provide all three behaviors.

Some will say, “just learn the Haxe API.” Perhaps. But perhaps API-mapping helpers (as static extensions) could ease the transition of users from dynamic languages, like JavaScript, Ruby, etc. e.g. imagine using RubyHelper; providing a similar set of .sub and .gsub functions. Gateway drugs. :smiley_cat:

Thus, I’m toying with static extensions that support static method overloading. That is, your static extension may provide overloaded methods. The macro works by 1) renaming the overloaded methods on the library, then within the class that’s using it, 2) simply trying to type each option at each call site. If I’m thinking correctly, this plays nicely with all existing Haxe magic (abstracts, to/from casts, optional args, etc) by simply allowing the compiler to decide what works.

Bkg / aside: I’ve played with various methods of overloading for a while. One early attempt was using macro functions as the overloaded methods themselves. It wasn’t bad, but the main issue was it was ugly actually writing the overloaded methods (you needed #if macro and #else in just the right places and some messy boiler plate…)

I do like that the static extension approach makes writing overloaded methods perfectly natural. And limiting to static extensions keeps the overhead down (it scans classes for their usings, and only searches when they are implement SEMacro.Overloaded.) On my medium/large project, a global macro injection only cost starts at around 5% overhead. I’ll have to measure as I start to use it more.

It’s also haxelib package friendly: If you write an overloaded SE library, and it depends on my SEMacro library (which will include the global metadata injection), then it all should just work.

What does it look like? Here’s my overloaded String test library:

Pretty unsurprising, eh? And here’s the class that uses it:

Again, pretty unsurprising. Which prints:

image

Naturally, code completion doesn’t really work… Well, kind of… All three renamed methods show up, and their completion does work normally:

But if you try to use the actual replace() function, you see a funky, unrelated SEMacro internal signature:

image

Clearly this approach doesn’t (yet) support runtime calls… it’d be a bit of work to support that automatically. You could allow static extension authors to write a runtime dynamic selection function. Or you could decide to only support static inline methods, nullifying the runtime issue entirely.

And of course, I’m still working out a couple details. :wink:

Seems like an interesting approach to me. Thoughts? Do you love / hate overloading? Like the static extension workflow? What other overloaded APIs / language-helpers would benefit?


(Luke) #2

I think overloading is a very useful feature to have when you have a class with multiple functions that all do basically the same thing, but just have a few values tweaked here and there, which could otherwise have been passed in as additional arguments.

Haxe has optional arguments and default value arguments, so you could argue (no pun intended) that you can just make the parameters optional, but that can complicate functions and sometimes you don’t know which order you need your parameters in for it to work properly.

Also, you might have multiple parameters with the same data type but doing different things inside the function, so optional parameters just wouldn’t work in that instance and is where function overloading can be really useful.

Having a simple draw function with different types, such as Font, Image, Rect, etc. can really help to simplify graphics APIs. True, it’s not hard to remember drawString, drawImage, drawRect but it does simplify it. Also, longer function names can be very off-putting.

It might not seem like a beneficial feature, but when it is it makes life certainly a lot easier. I don’t see any immediate drawbacks to it (I don’t think there is any, honestly).


(Jeff Ward) #3

Ok, I’ve released an initial version, give it a try and let me know what you think!

See the readme for install and usage instructions, as well as current requirements and limitations.

Cheers!


(Jeff Ward) #4

Hmm, @singmajesty mentioned abstract, which got me thinking… Indeed, it turns out you can wrap up this implementation in an abstract instead of static extension, if you prefer, as long as you know the concrete type you’re abstracted over. I just need to make it a little more convenient to call that inner check_se macro function.

The worst part is… Now I have to rename my library. :laughing: And this thread. It’s not just for static extension anymore.


(Jeff Ward) #5

Got a first pass of proper completion support up and running thanks to @:overload (thanks for suggesting it, @skial and @Gama11)

compl

** Full disclosure: completion currently suffers from an issue where overloaded functions will mask any other function calls of the same name. I’m looking into it. :smile:


(Philippe) #6

Seem very clever! I’m curious about how the AST is modified :slight_smile:


(Jeff Ward) #7

Thanks Philippe.

First, the build macros for your “tools classes” (e.g. MyStringTools) rename the overloaded functions and store some data about the names of those functions, e.g. replace has 3 options.

A global metadata scans all classes for where you’re using MyStringTools;

In the classes where you’re using MyStringTools, it looks for function calls named replace. The downfall is, at build macro time, it cannot know enough type information to know if a given replace() is intended for overloading, or perhaps it’s entirely unrelated (e.g. unrelatedArrayUtils.replace(foo, bar) will also get transformed.)

So, every function call to replace() gets transformed into an expression macro. e.g.:

  s.replace("this", "that");
  unrelatedArrayUtils.replace(foo, bar)

Gets transformed to:

  OverloadMacro.check_se(s, 'replace', <other metadata>, ["this", "that"]);
  OverloadMacro.check_se(unrelatedArrayUtils, 'replace', <other metadata>, [foo, bar]);

  // generally:
  OverloadMacro.check_se(subject_expr, field_name, tools_class, num_options, params);
  • Sidenote: check_se calls recurse properly, so this also works:
    s.replace( "foo", s.replace(~/0-9/, "_") );

But the check_se expression macro (who has complete type information) can rescue the unrelatedArrayUtils call. It has the job of deciding whether this is an overload or not, and if so, which one. It first checks whether this is valid (by trying to Context.typeof on it):

subject_expr.field_name(<params>)

If so, then this is not an overloaded method, it’s a member method. (a.k.a. your unrelatedArrayUtils.replace call above is restored without interruption.)

If not, it then tries the N overloaded static extension methods:

tools_class.<field_name + suffix N>(<subject, params>)

If one of those types correctly, we’re done! If none, we error “didn’t find an overload”.

Getting completion to work, then, is a matter of: if we’re in display mode, don’t transform replace() function calls to check_se, instead transform them to a generated class with a replace method that has the proper @:overload meta to display all 3 signatures.

The above is how static extension works. As shown above, I’m playing with an abstract implementation. It’s actually safer and faster - it doesn’t require any using / scan. The abstract goes straight to the OverloadedMacro.check_se expression macro function. TBD: I might be able to write a macro function that decorates this check_se into the abstract, as well as completion @:overload meta.

It’s all an experiment. Surely there’s some case I forgot, and this whole thing will fall apart in the real world. :laughing:

Cheers!