Skip to content

Instantly share code, notes, and snippets.

@dpeek
Created October 7, 2011 13:35
Show Gist options
  • Save dpeek/1270290 to your computer and use it in GitHub Desktop.
Save dpeek/1270290 to your computer and use it in GitHub Desktop.
Typed Signals with Generics
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