Friendly types with haXe

in

If you are experienced with haXe you'll know that its type system permits to catch a lot of errors at compile time simply veryfing that parameter/member types are correctly used in your application. Members can only have two access modifiers: public or private. But what if you want to emulate something like the "friend" modifier that exists in other languages?

Let's see a practical example.

class Test {
  static function main() {
    var c = new Container();
    var n1 = c.add(new Item("John"));
    var n2 = c.add(new Item("Jane"));
    
    trace(c);
    trace(n1);
    trace(n2);
  }
}

class Container {
  public function new() {
    children = new List();
  }
  public function add(n : Item) {
    children.add(n);
    return n;
  }
  
  public inline function iterator() { return children.iterator(); }
  public inline function count() { return children.length; }
  
  public function toString() {
    return "[Container] has " + children.length + " children"; 
  }
  
  var children : List;
}

class Item {
  public var parent(default, null) : Container;
  public var name(default, null) : String;
  public function new(name : String) {
    this.name = name;
  }
  public function toString() {
    if(parent != null)
      return "[Item] " + name + " has " + (parent.count() - 1) + " sibblings"; 
    else
      return "[Item] " + name + " has no parent"; 
  }
}

This flat hierarchy proves my point because something is missing, the Item instances never receive a reference to their container and the field parent is always null. The output trace is:

Test.hx:8: [Container] has 2 children
Test.hx:9: Item John has no parent
Test.hx:10: Item Jane has no parent

I don't want to expose parent as public or pass it in the Item constructor because I want my hierarchy to be composed just using the method add (in real life a lot of things can lead to that decision).
With a friend I could just add this to Item:

// friend method
private function setParent(p) {
  parent = p;
}

But how do you call that safely from outside the Item class? It is private so it cannot be invoked directly ... you can use untyped to make the compiler skip the types check on that call; but what happens if you want to change the name of setParent to set_parent at a later point and forget to change the untyped call too? The compiler will not spot the error and you'll only catch it at runtime when some strange behavior will pop.

Here it comes the solution, change the Container.add function this way:

public function add(n : Item) {
  children.add(n);
  var p : { private function setParent(p : Container) : Void } = n;
  p.setParent(this);
  return n;
}

I have created a temporary var p whose type is exactly and only what we need to access the "friend" method. It works without any casting or untyped access because the type of "p" is structurally a sub-type of Item making it implicitly compatible. We can call setParent from p because typedefs define a structure but do not impose any access limit.

Now the output is what expected at first:

Test.hx:7: [Container] has 2 children
Test.hx:8: [Item] John has 1 sibblings
Test.hx:9: [Item] Jane has 1 sibblings

If and when you will change the name of Item.setParent the compiler will throw an error notifying that Item is no more compatible with the type of p.
Many thanks to Nicolas Cannasse that suggested this unusual way of using typedefs on the haXe mailing list.

Comments

I've just been facing exactly

I've just been facing exactly the same problem; dealt with it by creating an explicit typedef for 'friend' classes to use rather than a temporary/dynamic one. Hadn't thought of the temporary type - neat idea.

IanT