Skip to content

Instantly share code, notes, and snippets.

@skial
Last active June 10, 2016 06:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save skial/ed646b9594a1f2b3eb3885ae32f7250b to your computer and use it in GitHub Desktop.
Save skial/ed646b9594a1f2b3eb3885ae32f7250b to your computer and use it in GitHub Desktop.
Attempted exercise example based on Jeff Ward's mailing list topic, type safe pubsub https://groups.google.com/forum/#!topic/haxelang/xw5WrrssLzQ using Haxe 3.3.0. For Haxe 3.2 version, see http://try.haxe.org/#4ca5B
package;
import haxe.macro.Printer;
#if macro
import haxe.macro.Type;
import haxe.macro.Expr;
import haxe.macro.Context;
#end
/**
* ...
* @author Skial Bainn
*/
class Main {
static function main() {
var channel:PubSub<Colours> = [];
channel.sub(function(c) trace( 'The colour is: $c', c.r ));
var unsub1 = channel.sub(function(c) trace( 'Hello: $c' ));
var unsub2 = channel.sub(function(c) trace( 'World colour: $c' ));
channel.pub( Red );
unsub1();
channel.pub( Rgb(255, 128, 0) );
unsub2();
channel.pub( Blue );
}
}
enum EColours {
Red;
Blue;
Green;
Rgb(r:Int, g:Int, b:Int);
}
abstract Colours(EColours) from EColours to EColours {
/**
* This _can_ help the compile output smaller code.
*/
@:op(a.b) public static macro function resolve(ethis:Expr, name:String) {
var cases:Array<Case> = [];
var result = macro switch ($ethis) {
case Red: 0;
case Blue: 0;
case Green: 0;
case Rgb(r, g, b): switch ($v{name}) {
case 'r': r;
case 'g': g;
case 'b': b;
case _: -1;
}
};
try switch ( Context.typeof(ethis) ) {
case TAbstract(_.get() => a, p):
switch (a.type) {
case TEnum(_.get() => e, p):
for (key in e.constructs.keys()) {
if (e.constructs.get( key ).type.match(TFun(_, _))) {
var construct = e.constructs.get( key );
switch (construct.type) {
case TFun(args, ret):
var match = false;
for (arg in args) if (arg.name == name) {
match = true;
break;
}
if (match) {
var names = [for (a in args) if (a.name == name) macro $i { a.name } else macro _];
cases.push( {
// case Rgb(r, _, _):
values: [macro $i { key } ($a { names } )],
// r
expr: macro $i { name },
} );
}
case _:
}
}
}
case _:
}
case _:
} catch (e:Dynamic) {
}
if (cases.length != 0) {
result = {
expr: ESwitch(ethis, cases, macro null),
pos: ethis.pos,
}
}
return result;
}
}
typedef Subscribe<T> = T->Void;
typedef Unsubscribe = Void->Void;
abstract PubSub<T>(Array<Subscribe<T>>) from Array<Subscribe<T>> {
@:noCompletion public inline function subImpl(cb:Subscribe<T>):Unsubscribe {
this.push(cb);
return function() this.remove(cb);
}
public macro function sub<T>(ethis:Expr, cb:Expr):ExprOf<Unsubscribe> {
// Modify expressions.
return macro $ethis.subImpl($cb);
}
@:noCompletion public inline function pubImpl(data:T):Void {
for (subscriber in this) subscriber(data);
}
public macro function pub<T>(ethis:Expr, data:ExprOf<T>):Expr {
// Modify expressions.
return macro $ethis.pubImpl($data);
}
}
@jcward
Copy link

jcward commented Jun 9, 2016

Fascinating, I hadn't considered applying abstracts. I'll have to take some time to soak in this example.

@jcward
Copy link

jcward commented Jun 9, 2016

I love the elegant generic pubsub abstract. The main difference from my thoughts is that my handlers are listening to only one enum value (e.g. Green), and my handler signatures are type checked to match that value signature (in the case of Rgb, Int->Int->Int)

The resolve/extract macro is interesting.

I'll need to further consider if abstract enum can help me.

@jcward
Copy link

jcward commented Jun 9, 2016

Here's the public API of my PubSub:

class PubSub<T> {
  public function publish(msg:T);
  public static macro function subscribe(ps_instance:Expr, enum_constructor:Expr, handler:Expr);
}

So you can only publish valid enum values, and the subscribe macro ensures you can only add listeners to each enum value that have the proper listeners, like this:

using PubSub; // <-- sets ps_instance above

enum UIEvents {
  APP_START(t0:Float);
  ON_BUTTON_CLICK(btn:Button);
  MOUSE_MOVE(x:Int, y:Int);
  APP_TERMINATING;
}

var ps = new PubSub<UIEvents>;

// Publish takes only the enum values:
ps.publish(APP_START(new Date.getTime());

// Subscribe takes an enum constructor (not an instance), and a handler that
// must match the type of the enum parameters:
ps.subscribe(ON_BUTTON_CLICK, handle_click);
ps.subscribe(APP_TERMINATING, goodbye);
ps.subscribe(MOUSE_MOVE, handle_mouse_move); // compile error, this function doesn't take Int->Int

function handle_click(b) { // b is Button, same parameters as ON_BUTTON_CLICK
  trace('User clicked ${b.name}');
}

function goodbye() { }

function handle_mouse_move() {
}

@skial
Copy link
Author

skial commented Jun 9, 2016

Ah, that's much clearer. The abstract subscribe could possibly be turned into a macro method, that does the same or similar. I'll think about it some more.

@skial
Copy link
Author

skial commented Jun 9, 2016

Ok, so I've updated the gist with a new resolve, which is now macro powered, which basically outputs smaller code.

As for your use case, talking based on my gist and mind you, I haven't tried it, you might be able to return a type via a @:genericBuild class, a PubSub<T> that will only handle a specific enum construct, without any arguments, like in your example and only accept method with a matching signature.

// ---
// Pseudo code, borrowing types from the gist.
// ---
@:genericBuild(foobar)
class Channel<T> {}

class Main {

 public static function main() {
  var channel:Channel<Rgb> = [];
  // Channel<Rgb> should return a PubSub<Int->Int->Int> abstract type.
  channel.sub(function(r, g, b) { ... });
  channel.pub(Rgb(255, 128, 0));
  // PubSub::sub macro would be modified to look at the Rgb type signature, convert and pass it onto PubSub::subImpl, 
  // which by then the compiler should check the types for you.
 }

}

Let me know if this doesnt make sense 😁

@jcward
Copy link

jcward commented Jun 9, 2016

Fascinating, you've used a bunch of concepts I've not considered, I'll have to study it for a bit. What a great learning experience, thanks for thinking through it with me.

@skial
Copy link
Author

skial commented Jun 9, 2016

No problem, its been a great exercise for me as well. I still want to explore Dan's typed strings that he has used throughout hxnodejs and hxdefold. Also some of the concepts above has been gleaned from Juraj's work throughout his tink_* libs.

A quick note from experimenting, var channel:Channel<Rgb> = [] doesn't work. The compile expects a type or constant as far as Im aware, but var channel = new Channel(Rgb) does. You can get the call args through Context.getCallArguments(). I have a feeling though avoiding macros as best as possible might be the better solution.

@skial
Copy link
Author

skial commented Jun 10, 2016

Thinking about it again, what happens in your example if you publish a APP_START(new Date.getTime()) but have two subscribers, one that has subscribe(APP_START, handle_start) and one that actually wants an enum value, for whatever reason, subscribe(UIEvents, handle_anything)?

I really like the idea of matching method signatures against an enums constructor signature, unwrapping the values and passing them on as individual args, but this feature could stand on its own, outside a pubsub library.

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