-
-
Save boozook/bf62356e751ea7e60483 to your computer and use it in GitHub Desktop.
Signal builder using new Rest type parameter in Haxe
This file contains hidden or 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
package hx.event.test; | |
import haxe.unit.TestRunner; | |
class Main | |
{ | |
public static function main() | |
{ | |
var runner = new TestRunner(); | |
runner.add(new SignalTest()); | |
runner.run(); | |
#if sys | |
Sys.exit(runner.result.success ? 0 : 1); | |
#end | |
} | |
} |
This file contains hidden or 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
package hx.event; | |
import haxe.extern.Rest; | |
import haxe.Constraints; | |
@:genericBuild(hx.event.SignalMacro.build()) | |
class Signal<Rest> extends Base<Rest -> Void> | |
{ | |
public function dispatch(rest:Rest):Void {} | |
} | |
typedef Fugnal<T:Function> = Base<T>; | |
/* @:genericBuild(hx.event.SignalMacro.build()) | |
class Fugnal<T:Function> extends Base<T> | |
{ public var dispatch(default, never):T; } */ | |
typedef Opt<T> = T; | |
class Base<T:Function> | |
{ | |
//----------- properties, fields ------------// | |
var head:Slot<T>; | |
var tail:Slot<T>; | |
var toAddHead:Slot<T>; | |
var toAddTail:Slot<T>; | |
var dispatching:Bool; | |
//--------------- constructor ---------------// | |
public function new() | |
{ | |
dispatching = false; | |
} | |
//--------------- initialize ----------------// | |
public function dispose(slots:Bool = true):Void | |
{ | |
if(slots) | |
{ | |
end(); | |
var slot = head; | |
while(slot != null) | |
{ | |
slot.dispose(); | |
slot = slot.next; | |
} | |
for(s in [head, tail, toAddHead, toAddTail]) | |
if(s != null) | |
s.dispose(); | |
} | |
head = tail = toAddHead = toAddTail = null; | |
} | |
//---------------- control ------------------// | |
public function add(listener:T, ?once:Bool):Slot<T> | |
{ | |
var slot = new Slot(this, listener, once); | |
if(dispatching) | |
{ | |
if(toAddHead == null) | |
toAddHead = toAddTail = slot; | |
else | |
{ | |
toAddTail.next = slot; | |
slot.previous = toAddTail; | |
toAddTail = slot; | |
} | |
} | |
else | |
{ | |
if(head == null) | |
head = tail = slot; | |
else | |
{ | |
tail.next = slot; | |
slot.previous = tail; | |
tail = slot; | |
} | |
} | |
return slot; | |
} | |
/** Look like a stopPropagation **/ | |
public function stop():Void if(dispatching) end(); | |
// TODO: public EitherType<Slot, T:Function> | |
public function remove(slot:Slot<T>):Void | |
{ | |
if(head == slot) | |
head = head.next; | |
if(tail == slot) | |
tail = tail.previous; | |
if(toAddHead == slot) | |
toAddHead = toAddHead.next; | |
if(toAddTail == slot) | |
toAddTail = toAddTail.previous; | |
if(slot.previous != null) | |
slot.previous.next = slot.next; | |
if(slot.next != null) | |
slot.next.previous = slot.previous; | |
} | |
inline function start():Void | |
{ | |
dispatching = true; | |
} | |
function end():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; | |
} | |
} | |
//----------- handlers, callbacks -----------// | |
//--------------- accessors -----------------// | |
} | |
@:allow(hx.event.Base) | |
@:access(hx.event.Base) | |
class Slot<T:Function> | |
{ | |
var signal:Base<T>; | |
var listener:T; | |
var once:Bool; | |
var previous:Slot<T>; | |
var next:Slot<T>; | |
public var disposed(get_disposed, never):Bool; | |
function new(signal:Base<T>, listener:T, once:Bool) | |
{ | |
this.signal = signal; | |
this.listener = listener; | |
this.once = once; | |
} | |
//--------------- initialize ----------------// | |
public function dispose():Void | |
{ | |
if(signal != null) | |
{ | |
signal.remove(this); | |
signal = null; | |
} | |
listener = null; | |
} | |
//--------------- accessors -----------------// | |
public function get_disposed():Bool | |
return listener == null && signal == null; | |
} |
This file contains hidden or 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
package hx.event; | |
import haxe.macro.Compiler; | |
import hx.event.Signal.Opt; | |
import haxe.macro.Printer; | |
import haxe.macro.Context; | |
import haxe.macro.Expr; | |
import haxe.macro.Type; | |
using haxe.macro.Tools; | |
class SignalMacro | |
{ | |
//----------- properties, fields ------------// | |
static var signalTypes = new Map<Int, Map<String, Bool>>(); | |
static inline var NAME_SIGNAL = "Signal"; | |
static inline var NAME_FUGNAL = "Fugnal"; | |
static inline var SUPER = "Base"; | |
static inline var METHOD = "dispatch"; | |
static var PACK(get, never):Array<String>; | |
//---------------- control ------------------// | |
static function build():ComplexType | |
{ | |
return switch(Context.getLocalType()) | |
{ | |
case TInst(_.get() => {name: NAME_SIGNAL}, params): | |
buildSignalClass(params, NAME_SIGNAL); | |
case Type.TInst(_.get() => {name: NAME_FUGNAL}, params): | |
{ | |
var args:Array<{name:String, opt:Bool, t:Type}> = switch(params[0]) | |
{ | |
case Type.TFun(args, ret): args; | |
default: [{t:Type.TMono({get:function()return null, toString:function() return "TMono(Null)"}), opt:false, name:"momo"}]; | |
} | |
buildFugnalClass(args); | |
} | |
default: | |
throw Context.error("Wrong type. SignalMacro.build can build only Signal generic class.", Context.currentPos()); | |
} | |
} | |
static function buildFugnalClass(args:Array<{name:String, opt:Bool, t:Type}>):ComplexType | |
{ | |
var params:Array<Type> = []; | |
for(i in 0...args.length) | |
{ | |
var t = args[i].t; | |
if(args[i].opt) | |
t = wrapToOptional(t); | |
params[i] = t; | |
} | |
var result = buildSignalClass(params, NAME_FUGNAL); | |
switch(result) | |
{ | |
case ComplexType.TPath(tp): | |
{ | |
tp.sub = tp.name; | |
tp.name = NAME_SIGNAL; | |
} | |
default: | |
} | |
return result; | |
} | |
static function buildSignalClass(params:Array<Type>, baseName:String):ComplexType | |
{ | |
function filter(type:Type):Bool return switch(type.follow()) | |
{ | |
case Type.TAbstract(_.get() => {name: "Void"}, _): false; | |
default: true; | |
}; | |
params = params.filter(filter); | |
var numParams = params.length; | |
var optParams = isOptionalTypes(params); | |
var optParamsID = followOptionalTypes(params); | |
var name = baseName + numParams + optParamsID; | |
if(!signalTypes.exists(numParams) || !(signalTypes.get(numParams):Map<String, Bool>).exists(name)) | |
{ | |
var typeParams:Array<TypeParamDecl> = []; | |
var superClassFunctionArgs:Array<ComplexType> = []; | |
var dispatchArgs:Array<FunctionArg> = []; | |
var listenerCallParams:Array<Expr> = []; | |
for(i in 0...numParams) | |
{ | |
var opt = optParams[i]; | |
var argT:ComplexType = TPath({name: 'T$i', pack: []}); | |
// additional wrap into `Null<>` | |
//if(opt) argT = TPath({pack: [], name: "Null", params: [TypeParam.TPType(argT)]}); | |
typeParams.push({name: 'T$i'}); | |
superClassFunctionArgs.push(opt ? TOptional(argT) : argT); | |
dispatchArgs.push({name: 'arg$i', type: argT, opt:opt}); | |
listenerCallParams.push(macro $i{'arg$i'}); | |
} | |
var pos = Context.currentPos(); | |
Context.defineType( | |
{ | |
pack: PACK, | |
name: name, | |
pos: pos, | |
params: typeParams, | |
kind: TDClass( | |
{ | |
pack: PACK, | |
name: baseName, | |
sub: SUPER, | |
params: [TPType(TFunction(superClassFunctionArgs, macro :Void))] | |
}), | |
fields: [ | |
{ | |
name: METHOD, | |
access: [APublic], | |
pos: pos, | |
kind: FFun( | |
{ | |
args: dispatchArgs, | |
ret: macro :Void, | |
expr: macro | |
{ | |
start(); | |
var slot = head; | |
while(dispatching && slot != null) | |
{ | |
slot.listener($a{listenerCallParams}); | |
if(slot.once) | |
slot.dispose(); | |
slot = slot.next; | |
} | |
end(); | |
} | |
}) | |
} | |
] | |
}); | |
var map = signalTypes.get(numParams); | |
if(map == null) | |
signalTypes.set(numParams, (map = new Map())); | |
map.set(name, true); | |
} | |
return ComplexType.TPath({pack: PACK, name: name, params: [for(t in params) TPType(t.toComplexType())]}); | |
} | |
static inline function isOptional(type:Type):Bool | |
{ | |
return switch(type) | |
{ | |
case Type.TType(_.get() => {name: "Null"}, params): | |
#if NullIsNotOptional false; #else true; #end | |
case Type.TType(_.get() => {name: "Opt"}, params): | |
true; | |
default: | |
false; | |
} | |
} | |
static function isOptionalTypes(types:Array<Type>):Array<Bool> | |
{ | |
return [for(i in 0...types.length) isOptional(types[i])]; | |
} | |
static function followOptionalTypes(types:Array<Type>):OrderingID | |
{ | |
var opts = 0; | |
var result = ""; | |
for(i in 0...types.length) | |
{ | |
var isOpt = isOptional(types[i]); | |
result += isOpt ? "o" : "r"; | |
while(isOptional(types[i])) | |
types[i] = types[i].follow(true); | |
opts++; | |
} | |
return opts > 0 ? "_" + result : ""; | |
} | |
static inline function wrapToOptional(type:Type):Type | |
{ | |
var opt:Type = Context.getType("hx.event.Opt"); | |
return switch(opt) | |
{ | |
case Type.TType(_.get() => {name: "Opt"}, params): | |
params[0] = type; | |
opt; | |
default: type; | |
} | |
} | |
//--------------- accessors -----------------// | |
static inline function get_PACK() return ["hx", "event"]; | |
} | |
private typedef OrderingID = String; |
This file contains hidden or 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
package hx.event.test; | |
import haxe.io.Bytes; | |
import hx.event.Signal; | |
import hx.event.Signal.Fugnal; | |
import haxe.extern.EitherType; | |
import haxe.unit.TestCase; | |
class SignalTest extends TestCase | |
{ | |
//----------- properties, fields ------------// | |
//--------------- constructor ---------------// | |
public function new() | |
{ | |
super(); | |
} | |
//----------------- tests -------------------// | |
public function testSignal():Void | |
{ | |
var signal = new Signal<Int, String>(); | |
var data:{a:Int, b:String}; | |
var conn = signal.add(function(a, b) data = {a:a, b:b} ); | |
signal.dispatch(42, "test"); | |
assertTrue(data != null); | |
assertEquals(42, data.a); | |
assertEquals("test", data.b); | |
signal.dispatch(1, ""); | |
assertTrue(data != null); | |
assertEquals(1, data.a); | |
assertEquals(0, data.b.length); | |
} | |
public function testSignalOnce():Void | |
{ | |
var signal = new Signal<Int, String>(); | |
var data:{a:Int, b:String}; | |
var conn = signal.add(function(a, b) data = {a:a, b:b} ); | |
signal.dispatch(42, "test"); | |
assertTrue(data != null); | |
assertEquals(42, data.a); | |
assertEquals("test", data.b); | |
signal.dispatch(1, ""); | |
assertTrue(data != null); | |
assertEquals(1, data.a); | |
assertEquals(0, data.b.length); | |
} | |
public function testSignalOption():Void | |
{ | |
//var signal = new Signal<Int, Null<String>>(); | |
var signal = new Signal<Int, Opt<Bytes>>(); | |
var data:{a:Int, b:Bytes}; | |
var conn = signal.add(function(a, ?b) data = {a:a, b:b} ); | |
signal.dispatch(42, Bytes.alloc(42)); | |
assertTrue(data != null); | |
assertEquals(42, data.a); | |
assertEquals(42, data.b.length); | |
signal.dispatch(1, null); | |
assertTrue(data != null); | |
assertEquals(1, data.a); | |
assertEquals(null, data.b); | |
// assertEquals(0, data.b.length); | |
} | |
public function testSignalTyping():Void | |
{ | |
var signalA:Signal = new Signal(); | |
var signalB:Signal<Void> = new Signal(); | |
var signalC = new Signal<Opt<Null<String>>>(); | |
var signalD = new Signal<Opt<String>, Bool>(); | |
var signalE = new Signal<Opt<Null<String>>, Bool>(); | |
var signalF = new Signal<Null<String>>(); | |
var signalG = new Signal<EitherType<Array<Float>, Float>, Array<String>>(); | |
var signalH = new Signal<Null<EitherType<Array<Float>, Float>>, String>(); | |
var signalJ:Signal<Dynamic, Array<String>> = new Signal<EitherType<Array<Float>, Float>, Array<String>>(); | |
//TODO: | |
//var signalK:Signal<Dynamic> = new Signal(); | |
//var signalL:Signal<Int, Dynamic> = new Signal(); | |
//var signalM:Signal<Opt<Dynamic>, Array<String>> = new Signal<EitherType<Array<Float>, Float>, Array<String>>(); | |
$type(signalA).dispatch(); | |
$type(signalB).dispatch(); | |
$type(signalC).dispatch(); | |
$type(signalD); | |
$type(signalD.dispatch); | |
signalD.dispatch(true); | |
signalD.dispatch("", true); | |
$type(signalE); | |
$type(signalE.dispatch); | |
signalE.dispatch(true); | |
signalE.dispatch("", true); | |
$type(signalF); | |
$type(signalF.dispatch); | |
signalF.dispatch(); | |
$type(signalF); | |
$type(signalF.dispatch); | |
signalF.dispatch(); | |
$type(signalG); | |
$type(signalG.dispatch); | |
signalG.dispatch([], []); | |
signalG.dispatch(42, [""]); | |
signalG.dispatch(42., [""]); | |
signalG.dispatch(null, []); | |
$type(signalH); | |
$type(signalH.dispatch); | |
signalH.dispatch(""); | |
$type(signalJ); | |
$type(signalJ.dispatch); | |
signalJ.dispatch(null, []); | |
signalJ.dispatch("", []); | |
signalJ.dispatch(42, []); | |
signalJ.dispatch([], []); | |
/// | |
/*$type(signalM); | |
$type(signalM.dispatch); | |
signalM.dispatch([]); | |
signalM.dispatch(null, []); | |
signalM.dispatch("", []); | |
signalM.dispatch(42, []); | |
signalM.dispatch([], []);*/ | |
assertTrue(true); | |
} | |
public function testSignalDispatch():Void | |
{ | |
// Ordinar: | |
var counter:Int = 0; | |
var signal = new Signal(); | |
signal.add(function() counter++); | |
signal.dispatch(); | |
signal.dispatch(); | |
assertEquals(2, counter); | |
// Once | |
counter = 0; | |
signal.add(function() counter++, true); | |
signal.dispatch(); | |
assertEquals(2, counter); | |
signal.dispatch(); | |
assertEquals(3, counter); | |
signal = new Signal(); | |
signal.dispose(); | |
// Once & Ordinar: | |
counter = 0; | |
function handler() counter++; | |
signal.add(handler, true); | |
signal.add(handler, true); | |
signal.dispatch(); | |
assertEquals(2, counter); | |
counter = 0; | |
signal.add(handler, false); | |
signal.add(handler, false); | |
signal.add(handler, false); | |
signal.add(handler, true); | |
signal.dispatch(); | |
assertEquals(4, counter); | |
signal.dispatch(); | |
assertEquals(4 + 3, counter); | |
} | |
public function testDispose():Void | |
{ | |
var counter = 0; | |
var signal = new Signal(); | |
var a = signal.add(function() counter++); | |
var b = signal.add(function() counter++, true); | |
var c = signal.add(function() counter++, true); | |
signal.dispatch(); | |
assertEquals(3, counter); | |
counter = 0; | |
a.dispose(); | |
signal.dispatch(); | |
assertEquals(0, counter); | |
b.dispose(); | |
c.dispose(); | |
// test dispose in dispatching: | |
a = signal.add(function() counter++); | |
b = signal.add(function() {counter++; c.dispose();}, true); | |
c = signal.add(function() counter++, true); | |
signal.dispatch(); | |
assertEquals(2, counter); | |
counter = 0; | |
a.dispose(); | |
b.dispose(); | |
c.dispose(); | |
signal.dispatch(); | |
assertEquals(0, counter); | |
// test dispose in dispatching: | |
a = signal.add(function() {counter++; b.dispose();}); | |
b = signal.add(function() counter++); | |
c = signal.add(function() counter++); | |
signal.dispatch(); | |
assertEquals(2, counter); | |
signal.dispatch(); | |
assertEquals(4, counter); | |
counter = 0; | |
a.dispose(); | |
b.dispose(); | |
c.dispose(); | |
signal.dispatch(); | |
assertEquals(0, counter); | |
// test dispose in dispatching: | |
a = signal.add(function() {counter++; signal.dispose(false);}); // + | |
b = signal.add(function() counter += 2); // + | |
c = signal.add(function() counter += 10); // + | |
signal.dispatch(); | |
assertEquals(13, counter); | |
signal.dispatch(); | |
assertEquals(13, counter); | |
// test dispose in dispatching: | |
counter = 0; | |
var signal = new Signal(); | |
a = signal.add(function() {counter++; signal.dispose(true);}); // + | |
b = signal.add(function() counter += 2); // - | |
c = signal.add(function() counter += 10); // - | |
signal.add(function() counter += 100); // - | |
signal.add(function() counter += 10000); // - | |
signal.dispatch(); | |
assertEquals(1, counter); | |
signal.dispatch(); | |
assertEquals(1, counter); | |
} | |
public function testStopPropagation():Void | |
{ | |
var counter = 0; | |
var signal = new Signal(); | |
var a = signal.add(function() counter++); | |
var b = signal.add(function() {counter++; signal.stop();}); | |
var c = signal.add(function() counter++); | |
signal.dispatch(); | |
assertEquals(2, counter); | |
counter = 0; | |
a.dispose(); | |
b.dispose(); | |
c.dispose(); | |
signal.dispatch(); | |
assertEquals(0, counter); | |
a = signal.add(function() {counter++; signal.stop();}); | |
b = signal.add(function() counter++); | |
c = signal.add(function() counter++); | |
signal.dispatch(); | |
assertEquals(1, counter); | |
signal.dispatch(); | |
assertEquals(2, counter); | |
counter = 0; | |
a.dispose(); | |
b.dispose(); | |
c.dispose(); | |
signal.dispatch(); | |
assertEquals(0, counter); | |
counter = 0; | |
var signal = new Signal(); | |
a = signal.add(function() counter++); | |
b = signal.add(function() counter += 2); | |
c = signal.add(function() counter += 10); | |
signal.add(function() {counter += 100; signal.stop();}); | |
signal.add(function() counter += 10000); | |
signal.dispatch(); | |
assertEquals(113, counter); | |
signal.dispatch(); | |
assertEquals(113 * 2, counter); | |
} | |
public function testSignalHandlerTyping():Void | |
{ | |
var data:{a:String, b:Bool}; | |
var signal = new Signal<Opt<String>, Bool>(); | |
function handler(?a:String, b:Bool):Void data = {a:a, b:b}; | |
signal.add(handler); | |
signal.dispatch("test", true); | |
assertFalse(data == null); | |
assertEquals("test", data.a); | |
assertTrue(data.b); | |
data = null; | |
signal.dispatch(true); | |
assertFalse(data == null); | |
assertEquals(null, data.a); | |
assertTrue(data.b); | |
// a : String -> b : Bool -> Void should be ?String -> Bool -> Void | |
//signal.add(function(a:String, b:Bool){}); | |
// a : String -> ?b : Null<Bool> -> Void should be ?String -> Bool -> Void | |
//signal.add(function(a:String, ?b:Bool){}); | |
signal.add(function(?a:String, ?b:Bool){}); | |
signal.dispatch(true); | |
} | |
public function testSignalCall():Void | |
{ | |
var data:{a:String, b:Bool}; | |
var signal = new Signal<Opt<String>, Bool>(); | |
function handler(?a:String, b:Bool):Void data = {a:a, b:b}; | |
signal.add(handler); | |
signal.dispatch("test", true); | |
assertFalse(data == null); | |
assertEquals("test", data.a); | |
assertTrue(data.b); | |
data = null; | |
signal.dispatch(true); | |
assertFalse(data == null); | |
assertEquals(null, data.a); | |
assertTrue(data.b); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment