Skip to content

Instantly share code, notes, and snippets.

@nadako
Last active November 27, 2020 16:08
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save nadako/b086569b9fffb759a1b5 to your computer and use it in GitHub Desktop.
Save nadako/b086569b9fffb759a1b5 to your computer and use it in GitHub Desktop.
Signal builder using new Rest type parameter in Haxe
class Main {
static function main() {
var signal = new Signal<Int,String>();
var conn = signal.connect(function(a, b) {
trace('Well done $a $b');
});
signal.dispatch(10, "lol");
}
}
import haxe.Constraints.Function;
@:genericBuild(SignalMacro.build())
class Signal<Rest> {}
class SignalBase<T:Function> {
var head:SignalConnection<T>;
var tail:SignalConnection<T>;
var toAddHead:SignalConnection<T>;
var toAddTail:SignalConnection<T>;
var dispatching:Bool;
public function new() {
dispatching = false;
}
public function connect(listener:T, once = false):SignalConnection<T> {
var conn = new SignalConnection(this, listener, once);
if (dispatching) {
if (toAddHead == null) {
toAddHead = toAddTail = conn;
} else {
toAddTail.next = conn;
conn.previous = toAddTail;
toAddTail = conn;
}
} else {
if (head == null) {
head = tail = conn;
} else {
tail.next = conn;
conn.previous = tail;
tail = conn;
}
}
return conn;
}
function disconnect(conn:SignalConnection<T>):Void {
if (head == conn)
head = head.next;
if (tail == conn)
tail = tail.previous;
if (toAddHead == conn)
toAddHead = toAddHead.next;
if (toAddTail == conn)
toAddTail = toAddTail.previous;
if (conn.previous != null)
conn.previous.next = conn.next;
if (conn.next != null)
conn.next.previous = conn.previous;
}
inline function startDispatch():Void {
dispatching = true;
}
function endDispatch():Void {
dispatching = false;
if (toAddHead != null) {
if (head == null) {
head = toAddHead;
tail = toAddTail;
} else {
tail.next = toAddHead;
toAddHead.previous = tail;
tail = toAddTail;
}
toAddHead = toAddTail = null;
}
}
}
@:allow(SignalBase)
@:access(SignalBase)
class SignalConnection<T:Function> {
var signal:SignalBase<T>;
var listener:T;
var once:Bool;
var previous:SignalConnection<T>;
var next:SignalConnection<T>;
function new(signal:SignalBase<T>, listener:T, once:Bool) {
this.signal = signal;
this.listener = listener;
this.once = once;
}
public function dispose():Void {
if (signal != null) {
signal.disconnect(this);
signal = null;
}
}
}
#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;
using haxe.macro.Tools;
class SignalMacro {
static function build():ComplexType {
return switch (Context.getLocalType()) {
case TInst(_.get() => {name: "Signal"}, params):
buildSignalClass(params);
default:
throw "assert";
}
}
static function buildSignalClass(params:Array<Type>):ComplexType {
var numParams = params.length;
var name = 'Signal$numParams';
var typeExists = try { Context.getType(name); true; } catch (_:Any) false;
if (!typeExists) {
var typeParams:Array<TypeParamDecl> = [];
var superClassFunctionArgs:Array<ComplexType> = [];
var dispatchArgs:Array<FunctionArg> = [];
var listenerCallParams:Array<Expr> = [];
for (i in 0...numParams) {
typeParams.push({name: 'T$i'});
superClassFunctionArgs.push(TPath({name: 'T$i', pack: []}));
dispatchArgs.push({name: 'arg$i', type: TPath({name: 'T$i', pack: []})});
listenerCallParams.push(macro $i{'arg$i'});
}
var pos = Context.currentPos();
Context.defineType({
pack: [],
name: name,
pos: pos,
params: typeParams,
kind: TDClass({
pack: [],
name: "Signal",
sub: "SignalBase",
params: [TPType(TFunction(superClassFunctionArgs, macro : Void))]
}),
fields: [
{
name: "dispatch",
access: [APublic],
pos: pos,
kind: FFun({
args: dispatchArgs,
ret: macro : Void,
expr: macro {
startDispatch();
var conn = head;
while (conn != null) {
conn.listener($a{listenerCallParams});
if (conn.once)
conn.dispose();
conn = conn.next;
}
endDispatch();
}
})
}
]
});
}
return TPath({pack: [], name: name, params: [for (t in params) TPType(t.toComplexType())]});
}
}
#end
@AustinEast
Copy link

AustinEast commented Feb 2, 2020

Howdy, I just tried this out in a project and it works great! But I've run into an issue where the second time I compile my project with the language server, I get the error "Type name util.Signal1 is redefined from module util.Signal1".

Looks like this issue is currently affecting other folks (HaxeFoundation/haxe#8368), but I was wondering if you've seen the issue when using this Signal implementation and had any tips?

Edit: Looks like this only occurs when I have the file open (in VS Code) that actually defines the Signal. For example, in my file Components.hx, I define a variable with the type of Signal<Component>. When I compile with that file open, it errors. When I close that file, it builds just fine. Weird!

@nadako
Copy link
Author

nadako commented Feb 3, 2020

Hmm, yeah this macro is not cache-friendly. I think the signalTypes map is reset on every compilation, so it tries to define the type again. What we should do here instead is try Context.getType to see if the type is already there. I'll change the snipper when I get some time.

@AustinEast
Copy link

AustinEast commented Feb 3, 2020

Awesome! I was actually messing with Context.getType to fix it myself, so i’ll update if i get something working before you’re able to.

Edit: I added in this method to SignalMarco.hx:

static function typeExists(typeName:String):Bool {
    try {
      if (Context.getType(typeName) != null) return true;
    } catch (error:String) {}

    return false;
  }

and replaced line 23 with:

if (!typeExists('$name')) {

This seems to work! I ran into an issue with Hashlink (every other build it would error with JIT ERROR 0 (jit.c line 3527)), but I think that's due to something going on with Hashlink, not specific to this macro.

On another note, have you thought about submitting this to haxelib? this is the best signal implementation i’ve found for haxe, but i was only able to find it because someone (Gama11 😄) linked it to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment