Skip to content

Instantly share code, notes, and snippets.

@frabbit
Last active December 19, 2015 02:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save frabbit/5885145 to your computer and use it in GitHub Desktop.
Save frabbit/5885145 to your computer and use it in GitHub Desktop.
type class description
package ;
using TestX.Eqs;
// consider the following interfaces
interface Compare<T> {
public function compare(b:T):Int;
}
interface Eq<T> {
public function eq(b:T):Bool;
}
// in the oop world you have to implement these 2 interface
// when you define your own type to make it compatible with these interfaces.
// This would look like this:
class MyClass implements Compare<MyClass> implements Eq<MyClass> {
public var x : Int;
public function new (x:Int) {
this.x = x;
}
public function eq(b:MyClass):Bool {
return this.x == b.x;
}
public function compare(b:MyClass):Int {
return this.x < b.x ? -1 : this.x > b.x ? 1 : 0;
}
}
// The implementation has to be done when you define your type, which means you
// have know at this point which interfaces you need. But what do you do if you need
// to implement another interface later?
// If it's your own type it's not a big deal, but how about making already existing types
// (types from other libraries) instances of Compare or Eq?
// This is not possible without some kind of wrapper around it
// (but the wrapper doesn't preserve the type).
// A workaround in haxe would be to define Compare and Eq
// as typedefs, but in that case already exisiting types must be shaped exactly
// like the typedef, which means functions must have the same name and signature.
// Here is the typeclass approach. Type classes are defined like interfaces in OOP,
// but they don't have a this type, they expect both types as
// parameters.
// In this example i use the suffix X for type class implementations
// to distinguish them from the OOP example.
interface CompareX<T> {
public function compare(a:T, b:T):Int;
}
interface EqX<T> {
public function eq(a:T, b:T):Bool;
}
class MyClassX {
public var x:Int;
public function new (x:Int) {
this.x = x;
}
}
// the implementations of type classes (think Interfaces) are decoupled of the type itself.
// so it's not a big deal to implement them for already exisiting types.
class MyClassXCompare implements CompareX<MyClassX> {
public function new () {}
public function compare(a:MyClassX, b:MyClassX):Int {
return a.x < b.x ? -1 : a.x > b.x ? 1 : 0;
}
}
class MyClassXEq implements EqX<MyClassX> {
public function new () {}
public function eq(a:MyClassX, b:MyClassX):Bool {
return a.x == b.x;
}
}
// for all typeclass functions in haxe it is usefull to have a static function with the same
// functionality, it's important to note that the typeclass itself becomes a function parameter.
class Eqs {
public static inline function eq<T>(a:T, b:T, eq:EqX<T>):Bool {
return eq.eq(a,b);
}
}
class Compares {
public static inline function compare<T>(a:T, b:T, c:CompareX<T>):Int {
return c.compare(a,b);
}
}
class TestX {
public static function main () {
// usage oop
new MyClass(1).eq(new MyClass(1));
// usage typeclass
Eqs.eq(new MyClassX(1), new MyClassX(2), new MyClassXEq());
// with using it becomes
new MyClassX(1).eq(new MyClassX(2), new MyClassXEq());
// so at this point you have to pass the instance of Eq explicit at call time
// which is nice because it decouples the data of the type from its methods.
// But it's also verbose, because you have to pass an instance of the
// interface/typeclass explicit.
// The Haskell compiler for example injects the type class at this point, so that
Eqs.eq(new MyClassX(1), new MyClassX(2));
// becomes:
Eqs.eq(new MyClassX(1), new MyClassX(2), new MyClassXEq());
// based on the type information of the parameters.
// In some way that's also what scuts does. It converts the call ("_" is no special
// syntax it's just a macro function)
Eqs.eq._(new MyClassX(1), new MyClassX(2));
// or
new MyClassX(1).eq._(new MyClassX(2));
// into something like this
Eqs.eq(new MyClassX(1), new MyClassX(2), new MyClassEqX()));
// it doesn't really use new at this point, it injects an instance of MyClassEqX
// from the context, this instance can be a static variable, a function or whatever.
// the macro "_" searches the context for an expression with the desired type.
// e.g. the expression Eqs.eq.bind(1,1,_) has needs an expression of type EqX<Int>
// as the last parameter.
// Because of it's decoupled structure, type classes are composable.
// You can use them on nested data types (check the ArrayEq instance below)
var a = [new MyClassX(1)];
var b = [new MyClassX(1)];
// compare arrays with MyClassX instances
trace(a.eq(b, new ArrayEq(new MyClassEq())));
// with type class injection in scuts it becomes
// (the required typeclass instances must be in scope)
trace(a.eq._(b);
}
}
class ArrayEq<T> implements EqX<Array<T>> {
// we need to know how we can compare the elements of the array
var eqT:EqX<T>;
public function new (eqT:EqX<T>) {
this.eqT = eqT;
}
public function eq(a:Array<T>, b:Array<T>):Bool {
if (a.length != b.length) return false;
for (i in 0...a.length) {
var e1 = a[i];
var e2 = b[i];
if (!eqT.eq(e1, e2)) return false;
}
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment