3

I have a common interface that describes access to the output stream like this:

interface IOutput {
    function writeInteger(aValue:Int):Void;
}

And I have an abstract implementation of this interface based on standard haxe.io.BytesOutput class:

abstract COutput(BytesOutput) from BytesOutput {
    public inline function new(aData:BytesOutput) {
        this = aData;
    }
    public inline function writeInteger(aValue:Int):Void {
        this.writeInt32(aValue);
    }
}

Though this abstract is truly implementing interface described above there's no direct reference to interface and when I'm trying to use it like this:

class Main {
    public static function out(aOutput:IOutput) {
        aOutput.writeInteger(0);
    }
    public static function main() {
        var output:COutput = new BytesOutput();
        out(output); // type error
    }
}

Compiler throws an error: COutput should be IOutput. I can solve this problem only through using common class that wraps BytesOutput and implements IOutput.

My question is how to show the Haxe compiler that the abstract implements the interface.

Gama11
  • 31,714
  • 9
  • 78
  • 100
meps
  • 579
  • 2
  • 17

2 Answers2

9

Abstracts can't implement interfaces because they're a compile-time feature and don't exist at runtime. This conflicts with interfaces, they do exist at runtime and dynamic runtime checks like Std.is(something, IOutput) have to work.

Haxe also has a mechanism called structural subtyping that can be used as an alternative to interfaces. With this approach, there's no need for an explicit implements declaration, it's good enough if something unifies with a structure:

typedef IOutput = {
    function writeInteger(aValue:Int):Void;
}

Unfortunately, abstracts aren't compatible with structural subtyping either due to the way they're implemented.


Have you considered using static extensions instead? At least for your simple example, that seems like the perfect solution for making a writeInteger() method available for any haxe.io.Output:

import haxe.io.Output;
import haxe.io.BytesOutput;
using Main.OutputExtensions;

class Main {
    static function main() {
        var output = new BytesOutput();
        output.writeInteger(0);
    }
}

class OutputExtensions {
    public static function writeInteger(output:Output, value:Int):Void {
        output.writeInt32(value);
    }
}

You could even combine this with structural subtyping so writeInteger() becomes available on anything that has a writeInt32() method (try.haxe link):

typedef Int32Writable = {
    function writeInt32(value:Int):Void;
}
Gama11
  • 31,714
  • 9
  • 78
  • 100
  • Thanks for the detailed explanation. I don't want to use static extensions because interfaces should hide internal protocol implementation from business logic. So it shouldn't be directly applied to the `BytesOutput` 'cause output object can be either XML or JSON formats. I'd rather use a wrapper class. – meps Jan 10 '19 at 21:00
  • Couldn't you use structural subtyping to accomplish that (in my example, typing things as `Int32Writable` instead of `BytesOutput`)? Then again, maybe the best solution is to simply create a subclass / wrapper class after all. :) – Gama11 Jan 10 '19 at 21:07
  • For my own case It's no difference between structural subtyping and interfaces. So I vote for the interfaces solution as more clear and robust. – meps Jan 10 '19 at 21:23
  • You could try using implicit casts to make the abstract conform to the IOutput type perhaps? https://haxe.org/manual/types-abstract-implicit-casts.html – Chii Jan 15 '19 at 11:22
0

As @Gama11 states, abstracts cannot implement interfaces. In Haxe, for type to implement an interface, it must be able to be compiled to something class-like that can be called using the interface’s methods without any magic happening. That is, to use a type as its interface, there needs to be a “real” class implementing that type. Abstracts in Haxe compile down to their base type—the abstract itself is entirely invisible after compilation happens. Thus, at runtime, there is no instance of a class with the methods defined in your abstract which implement the interface.

However, you can make your abstract appear to implement an interface by defining an implicit conversion to the interface you are trying to implement. For your example, the following might work:

interface IOutput {
    function writeInteger(aValue:Int):Void;
}

abstract COutput(BytesOutput) from BytesOutput {
    public inline function new(aData:BytesOutput) {
        this = aData;
    }
    @:to()
    public inline function toIOutput():IOutput {
        return new COutputWrapper((cast this : COutput));
    }
    public inline function writeInteger(aValue:Int):Void {
        this.writeInt32(aValue);
    }
}

class COutputWrapper implements IOutput {
    var cOutput(default, null):COutput;
    public function new(cOutput) {
        this.cOutput = cOutput;
    }
    public function writeInteger(aValue:Int) {
        cOutput.writeInteger(aValue);
    }
}

class Main {
    public static function out(aOutput:IOutput) {
        aOutput.writeInteger(0);
    }
    public static function main() {
        var output:COutput = new BytesOutput();
        out(output);
        out(output);
    }
}

Run on try.haxe.org

Note that, each time an implicit conversion happens, a new instance of the wrapper will be constructed. This may have performance implications. If you only access your value through its interface, consider setting the type of your variable to the interface rather than the abstract.

This is similar to “boxing” a primitive/value type in C#. In C#, value types, defined using the struct keyword, are allowed to implement interfaces. Like an abstract in Haxe, a value type in C# is compiled (by the JITter) into untyped code which simply directly accesses and manipulates the value for certain operations. However, C# allows structs to implement interfaces. The C# compiler will translate any attempt to implicitly cast a struct to an implemented interface into the construction of a wrapper class which stores a copy of the value and implements the interface—similar to our manually authored wrapper class (this wrapper class is actually generated by the runtime as part of JITing and is performed by the IL box instruction. See M() in this example). It is conceivable that Haxe could add a feature to automatically generate such a wrapper class for you like C# does for struct types, but that is not currently a feature. You may, however, do it yourself, as exemplified above.

binki
  • 7,754
  • 5
  • 64
  • 110