Last active
November 27, 2020 16:08
-
-
Save nadako/b086569b9fffb759a1b5 to your computer and use it in GitHub Desktop.
Signal builder using new Rest type parameter in Haxe
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
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
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 tryContext.getType
to see if the type is already there. I'll change the snipper when I get some time.