Created
October 7, 2011 13:35
-
-
Save dpeek/1270290 to your computer and use it in GitHub Desktop.
Typed Signals with Generics
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
class Main | |
{ | |
public static function main() | |
{ | |
new Main(); | |
} | |
public function new() | |
{ | |
var signal0 = new Signal0(); | |
signal0.add(test0); | |
var signal1 = new Signal1<String>(); | |
var slot1 = signal1.add(test1); | |
slot1.param1 = "foo"; | |
signal0.dispatch(); | |
signal1.dispatch("hello!"); | |
} | |
public function test0() | |
{ | |
trace("woot!"); | |
} | |
public function test1(value:String) | |
{ | |
trace(value); | |
} | |
} | |
class Signal<TSlot:Slot<Dynamic, Dynamic>, TListener> | |
{ | |
var slots:SlotList<TSlot, TListener>; | |
public var numListeners(get_numListeners, null):Int; | |
function get_numListeners() { return slots.length; } | |
public function new() | |
{ | |
slots = cast SlotList.NIL; | |
} | |
public function add(listener:TListener):TSlot | |
{ | |
return registerListener(listener); | |
} | |
public function addOnce(listener:TListener):TSlot | |
{ | |
return registerListener(listener, true); | |
} | |
public function remove(listener:TListener):TSlot | |
{ | |
var slot = slots.find(listener); | |
if (slot == null) return null; | |
slots = slots.filterNot(listener); | |
return slot; | |
} | |
public function removeAll():Void | |
{ | |
slots = cast SlotList.NIL; | |
} | |
function registerListener(listener:TListener, once:Bool=false, priority:Int=0):TSlot | |
{ | |
if (registrationPossible(listener, once)) | |
{ | |
var newSlot = createSlot(listener, once, priority); | |
slots = slots.prepend(newSlot); | |
return newSlot; | |
} | |
return slots.find(listener); | |
} | |
function registrationPossible(listener, once) | |
{ | |
if (!slots.nonEmpty) return true; | |
var existingSlot = slots.find(listener); | |
if (existingSlot == null) return true; | |
if (existingSlot.once != once) | |
{ | |
// If the listener was previously added, definitely don't add it again. | |
// But throw an exception if their once values differ. | |
throw "You cannot addOnce() then add() the same listener without removing the relationship first."; | |
} | |
return false; // Listener was already registered. | |
} | |
function createSlot(listener:TListener, once:Bool=false, priority:Int=0):TSlot | |
{ | |
return null; | |
} | |
} | |
class Signal0 extends Signal<Slot0, Void -> Void> | |
{ | |
public function new() | |
{ | |
super(); | |
} | |
public function dispatch() | |
{ | |
var slotsToProcess = slots; | |
while (slotsToProcess.nonEmpty) | |
{ | |
slotsToProcess.head.execute(); | |
slotsToProcess = slotsToProcess.tail; | |
} | |
} | |
override function createSlot(listener:Void -> Void, once:Bool=false, priority:Int=0) | |
{ | |
return new Slot0(this, listener, once, priority); | |
} | |
} | |
class Signal1<TValue1> extends Signal<Slot1<TValue1>, TValue1 -> Void> | |
{ | |
public function new() | |
{ | |
super(); | |
} | |
public function dispatch(value1:TValue1) | |
{ | |
var slotsToProcess = slots; | |
while (slotsToProcess.nonEmpty) | |
{ | |
slotsToProcess.head.execute(value1); | |
slotsToProcess = slotsToProcess.tail; | |
} | |
} | |
override function createSlot(listener:TValue1 -> Void, once:Bool=false, priority:Int=0) | |
{ | |
return new Slot1<TValue1>(this, listener, once, priority); | |
} | |
} | |
class Slot<TSignal:Signal<Dynamic, TListener>, TListener> | |
{ | |
var signal:TSignal; | |
public var listener(default, set_listener):TListener; | |
public var once(default, null):Bool; | |
public var priority(default, null):Int; | |
public var enabled:Bool; | |
public function new(signal:TSignal, listener:TListener, once:Bool=false, priority:Int=0) | |
{ | |
if (signal == null) throw "signal cannot be null"; | |
if (listener == null) throw "listener cannot be null"; | |
this.signal = signal; | |
this.listener = listener; | |
this.once = once; | |
this.priority = priority; | |
this.enabled = true; | |
} | |
public function remove() | |
{ | |
signal.remove(listener); | |
} | |
function set_listener(value:TListener):TListener | |
{ | |
if (value == null) throw "listener cannot be null"; | |
return listener = value; | |
} | |
} | |
class Slot0 extends Slot<Signal0, Void -> Void> | |
{ | |
public function new(signal:Signal0, listener:Void -> Void, once:Bool=false, priority:Int=0) | |
{ | |
super(signal, listener, once, priority); | |
} | |
public function execute() | |
{ | |
if (!enabled) return; | |
if (once) remove(); | |
listener(); | |
} | |
} | |
class Slot1<TValue1> extends Slot<Signal1<TValue1>, TValue1 -> Void> | |
{ | |
public var param1:TValue1; | |
public function new(signal:Signal1<TValue1>, listener:TValue1 -> Void, once:Bool=false, priority:Int=0) | |
{ | |
super(signal, listener, once, priority); | |
} | |
public function execute(value1:TValue1) | |
{ | |
if (!enabled) return; | |
if (once) remove(); | |
if (param1 != null) value1 = param1; | |
listener(value1); | |
} | |
} | |
class SlotList<TSlot:Slot<Dynamic, Dynamic>, TListener> | |
{ | |
/** | |
* Represents an empty list. Used as the list terminator. | |
*/ | |
public static var NIL = new SlotList<Dynamic, Dynamic>(null, null); | |
// Although those variables are not const, they would be if AS3 would handle it correctly. | |
public var head:TSlot; | |
public var tail:SlotList<TSlot, TListener>; | |
public var nonEmpty:Bool; | |
/** | |
* Creates and returns a new SlotList object. | |
* | |
* <p>A user never has to create a SlotList manually. | |
* Use the <code>NIL</code> element to represent an empty list. | |
* <code>NIL.prepend(value)</code> would create a list containing <code>value</code></p>. | |
* | |
* @param head The first slot in the list. | |
* @param tail A list containing all slots except head. | |
* | |
* @throws ArgumentException <code>ArgumentException</code>: Parameters head and tail are null. Use the NIL element instead. | |
* @throws ArgumentException <code>ArgumentException</code>: Parameter head cannot be null. | |
*/ | |
public function new(head:TSlot, tail:SlotList<TSlot, TListener>=null) | |
{ | |
nonEmpty = false; | |
if (head == null && tail == null) | |
{ | |
if (NIL != null) | |
{ | |
throw "Parameters head and tail are null. Use the NIL element instead."; | |
} | |
// this is the NIL element as per definition | |
nonEmpty = false; | |
} | |
else if (head == null) | |
{ | |
throw "Parameter head cannot be null."; | |
} | |
else | |
{ | |
this.head = head; | |
this.tail = (tail == null ? cast NIL : tail); | |
nonEmpty = true; | |
} | |
} | |
/** | |
* The number of slots in the list. | |
*/ | |
public var length(get_length, null):Int; | |
function get_length():Int | |
{ | |
if (!nonEmpty) return 0; | |
if (tail == NIL) return 1; | |
// We could cache the length, but it would make methods like filterNot unnecessarily complicated. | |
// Instead we assume that O(n) is okay since the length property is used in rare cases. | |
// We could also cache the length lazy, but that is a waste of another 8b per list node (at least). | |
var result = 0; | |
var p = this; | |
while (p.nonEmpty) | |
{ | |
++result; | |
p = p.tail; | |
} | |
return result; | |
} | |
/** | |
* Prepends a slot to this list. | |
* @param slot The item to be prepended. | |
* @return A list consisting of slot followed by all elements of this list. | |
* | |
* @throws ArgumentException <code>ArgumentException</code>: Parameter head cannot be null. | |
*/ | |
public function prepend(slot:TSlot) | |
{ | |
return new SlotList<TSlot, TListener>(slot, this); | |
} | |
/** | |
* Appends a slot to this list. | |
* Note: appending is O(n). Where possible, prepend which is O(1). | |
* In some cases, many list items must be cloned to | |
* avoid changing existing lists. | |
* @param slot The item to be appended. | |
* @return A list consisting of all elements of this list followed by slot. | |
*/ | |
public function append(slot:TSlot) | |
{ | |
if (slot == null) return this; | |
if (!nonEmpty) return new SlotList<TSlot, TListener>(slot); | |
// Special case: just one slot currently in the list. | |
if (tail == NIL) | |
{ | |
return new SlotList<TSlot, TListener>(slot).prepend(head); | |
} | |
// The list already has two or more slots. | |
// We have to build a new list with cloned items because they are immutable. | |
var wholeClone = new SlotList<TSlot, TListener>(head); | |
var subClone = wholeClone; | |
var current = tail; | |
while (current.nonEmpty) | |
{ | |
subClone = subClone.tail = new SlotList<TSlot, TListener>(current.head); | |
current = current.tail; | |
} | |
// Append the new slot last. | |
subClone.tail = new SlotList<TSlot, TListener>(slot); | |
return wholeClone; | |
} | |
/** | |
* Insert a slot into the list in a position according to its priority. | |
* The higher the priority, the closer the item will be inserted to the list head. | |
* @params slot The item to be inserted. | |
* | |
* @throws ArgumentException <code>ArgumentException</code>: Parameters head and tail are null. Use the NIL element instead. | |
* @throws ArgumentException <code>ArgumentException</code>: Parameter head cannot be null. | |
*/ | |
public function insertWithPriority(slot:TSlot) | |
{ | |
if (!nonEmpty) return new SlotList<TSlot, TListener>(slot); | |
var priority:Int = slot.priority; | |
// Special case: new slot has the highest priority. | |
if (priority > this.head.priority) return prepend(slot); | |
var wholeClone = new SlotList<TSlot, TListener>(head); | |
var subClone = wholeClone; | |
var current = tail; | |
// Find a slot with lower priority and go in front of it. | |
while (current.nonEmpty) | |
{ | |
if (priority > current.head.priority) | |
{ | |
var newTail = current.prepend(slot); | |
return new SlotList<TSlot, TListener>(head, newTail); | |
} | |
subClone = subClone.tail = new SlotList<TSlot, TListener>(current.head); | |
current = current.tail; | |
} | |
// Slot has lowest priority. | |
subClone.tail = new SlotList<TSlot, TListener>(slot); | |
return wholeClone; | |
} | |
/** | |
* Returns the slots in this list that do not contain the supplied listener. | |
* Note: assumes the listener is not repeated within the list. | |
* @param listener The function to remove. | |
* @return A list consisting of all elements of this list that do not have listener. | |
*/ | |
public function filterNot(listener:TListener) | |
{ | |
if (!nonEmpty || listener == null) return this; | |
if (listener == head.listener) return tail; | |
// The first item wasn't a match so the filtered list will contain it. | |
var wholeClone = new SlotList<TSlot, TListener>(head); | |
var subClone = wholeClone; | |
var current = tail; | |
while (current.nonEmpty) | |
{ | |
if (current.head.listener == listener) | |
{ | |
// Splice out the current head. | |
subClone.tail = current.tail; | |
return wholeClone; | |
} | |
subClone = subClone.tail = new SlotList<TSlot, TListener>(current.head); | |
current = current.tail; | |
} | |
// The listener was not found so this list is unchanged. | |
return this; | |
} | |
/** | |
* Determines whether the supplied listener Function is contained within this list | |
*/ | |
public function contains(listener:TListener):Bool | |
{ | |
if (!nonEmpty) return false; | |
var p = this; | |
while (p.nonEmpty) | |
{ | |
if (p.head.listener == listener) return true; | |
p = p.tail; | |
} | |
return false; | |
} | |
/** | |
* Retrieves the ISlot associated with a supplied listener within the SlotList. | |
* @param listener The Function being searched for | |
* @return The ISlot in this list associated with the listener parameter through the ISlot.listener property. | |
* Returns null if no such ISlot instance exists or the list is empty. | |
*/ | |
public function find(listener:TListener):TSlot | |
{ | |
if (!nonEmpty) return null; | |
var p = this; | |
while (p.nonEmpty) | |
{ | |
if (p.head.listener == listener) return p.head; | |
p = p.tail; | |
} | |
return null; | |
} | |
public function toString():String | |
{ | |
var buffer:String = ''; | |
var p = this; | |
while (p.nonEmpty) | |
{ | |
buffer += p.head + " -> "; | |
p = p.tail; | |
} | |
buffer += "NIL"; | |
return "[List "+buffer+"]"; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment