Phantom Types

(I’ve placed this in Shared Code rather than Articles because this is more me exploring a concept rather than writing a how-to on the subject. If it belongs better in the Articles section, let me know and I will move it.)

Referenced links:

Phantom Types in Java (Gabriel)
Phantom Types in Java (Maniagnosis)
Type Safe Builder Pattern in Java
Making Wrong Code Look Wrong

There have been a couple of articles posted to reddit regarding Phantom Types. They look interesting and potentially useful for gamedev but I’m not entirely sure. The appeal of this approach depends a fair bit on programmer preference (doesn’t everything!) and there are other ways to do what phantom types do (aren’t there always!) but I thought I would post this to share what I understand and see if others have thoughts.

Phantom types are a way to enforce certain constraints through the type system. In short, a Phantom Type is when a type is created without the intent that there be any instances of that type created. Let’s create some using interfaces.


public interface VecType { }

public interface Acceleration extends VecType {}
public interface Velocity extends VecType { }
public interfact Position extends VecType { }

Now let’s create a simple 2D Vector class.


public class Vec2<V extends VecType> {
   
    public final float x;
    public final float y;

    public Vec2(float xi, float yi) {
        x = xi;
        y = yi;
    }

    public void addTo(Vec2<V> v) {
        x += v.x;
        y += v.y;
    }
}

You can see that Vec2 takes a type parameter but no instances of V are created or stored. Parameterizing like this allows us to do something like the following:


Vec2<Position> p1 = new Vec2<Position>(4,5);   
Vec2<Position> p2 = new Vec2<Position(1,1);
Vec2<Velocity> v1 = new Vec2<Velocity>(5,0);


p1.add(p2); // Is fine, produces vector 5,6.

p1.add(v1); // compile time error, can't add a velocity to a position.

We are using the type system to prevent us from accidentally adding a velocity to a position. One difficulty is that type erasure prevents things that would make this really nice. For instance, I would like to be be able to do something like:


interface Unit { }
interface Meter extends Unit { }
interface Foot extends Unit { }

class Length<U extends Unit>  {

    public float value;

    public Length(float initialValue) {
        value = initialValue;
    }

    public static Length<Foot> convert(Length<Meter> l) {
        return new Length<Foot>(l.value*3.28);
    }

    public static Length<Meter> convert(Length<Foot> l) {
        return new Length<Meter>(l.value/3.28);
    }
}

It would be nice to be able to do this, but Java complains because after type erasure both convert methods have the same signature. Another use of phantom types is for things that you want to make it very explicit what type of input you want to receive. Imagine you have a game with an online scoreboard where people can enter their names. You want to prevent SQL injection attacks. You could do something like:


interface UserInput { }
interface Sanitized extends UserInput { }
interface Unsanitized extends UserInput { }


public class InputString<I extends UserInput> {
	
	final String value;

	private InputString(String value) {
		this.value = value;
	}


	public String getValue() {
		return value;
	}


	public static InputString<Unsanitized> makeInput(String val) {
		return new InputString<Unsanitized>(val);
	}


	public static InputString<Sanitized> sanitize(InputString<Unsanitized> input) {
		//  Lots of messy logic to sanitize a string against sql injection attacks
	}
}

By making the constructor private, we’ve ensured that the only way to create a Sanitized input is to run it through our sanitize method. If we look later in our code where this might be used


public void updateLeaderBoard(int score, InputString<Sanitized> playerName) {
	// logic around updating the database	
}

Here, the type helps better declare our intention, that the value must be sanitized and because the only way of getting an InputString is via your sanitize method, you can be sure (within the quality of sanitize method) that the value is in fact sanitized. Trying to submit an InputString will not fail at run time, it will fail at compile time.

Another situation may be encoding the State of a game entity.


interface FighterState { }
interface Hangar extends FighterState {} 
interface InFlight extends FighterState {}

public Fighter<S extends FighterState> extends Entity {
	
}

and then in some another class:


public  Fighter<InFlightState> launch(Fighter<InHangar> fgt) {
	//..code that launches the fighter
}

public  Fighter<HangarState> recover(Fighter<InFlight> fgt) {
	//...code the recovers the fighter
}

Like in the example above, the phantom type help makes it more explicit in the method signature what is being returned and what is expected. Trying to launch a Fighter will generate a compile time error. You can also see that we are able to extend by one type while also using the phantom type to parameterize.

There is no reason why you need to limit to a single type. An example of this is using a builder. Imagine you have a builder where you may not be able to (or may not want to) build the entire thing at once.


public interface State { }
public interface Defined extends State { }
public interface Undefined extends State { }


public LevelBuilder<A extends State, W extends State> {
	
	private LevelBuilder( ) { }

	/*
	Will not let you add World Data if it has already been defined.
	*/
	public static <S extends State> LevelBuilder<A,Undefined> addWorldData(LevelBuilder<S,Undefined> lb, ) { ... }

	/*
	Will not let you add Asset data if it has already been defined.
	*/
	public static <S extends State> LevelBuilder<Defined,S> addAssets(LevelBuilder<Undefined,S> lb, // asset parameters) {...}

	/*
	Only way to create a builder. Initializes it with both sets undefined to ensure that the 
	two pieces are initialized by the programmer.
	*/
	public static LevelBuilder<Undefined,Undefined> getBuilder() {...}
}


public class Level {
	

	private Level() { } // private constructor


	/**
	Only way to get a Level. Type signature ensures that a level can only be created from
	a bulider that has both WorldData and Assets defined.
	*/
	public static Level load(LevelBuilder<Defined,Defined> lb) {
		// ...build the level
	}
}

Some random final comments/summary:

  • If you’ve stuck it out this long, Thank you!

  • Overall I find phantom types to be interesting and worth considering. The more I work with type systems the more I like the idea of specifying as much as possible through them. I acknowledge that not everyone likes to work this way in which case phantom types may seem like overkill or over engineering.

  • They also seem to be a useful way to use the type system to better express/enforce programmer intent. Joel Spolsky has an article Making Wrong Code Look Wrong where they preface variables holding unsanitized strings with “us” (e.g. usUserName). But this is merely a convention and a programmer could accidentally or carelessly use usUserName where the sanitized version is required. Using phantom types like Input turns this convention into something enforced by the compiler.

  • Phantom types can also prevent certain types of errors (e.g. adding velocities to positions), although I am not sure how often those types of errors occurr and how bothersome they really are.

  • Type erasure limits the utility of phantom types in certian situations. Specifically it prevents overloading methods by the phantom type which could make things nicer.

  • We could achieve something similar to what phantom types achieve by extending base classes:


class Vec2 {...}

class PositionVec2 extends Vec2 {...

	// Only adds another position vector to this one.
	public void addTo(PositionVec2 v2) {...}

}

But due to single inheritance, you are then making your class structyre a lot less flexible. You may not want to tie your classes together this way. Also you can use multiple phantom types at a time where as with extension, you would have to create a subtype for each combination.

  • Similar results could also be handled by storing internal flags and doing checks but the failure will then be deferred until runtime.

class Vec2 {
	
	public VecType vType;

	public int x,y;

	public Vec2(VecType vType,int xi, int yi) { }

	//
	public void addTo(Vec2 v2) {
		if (vType != v2.vType) throw new IllegalArgumentException("Cannot add vectors of different types");
		// ...
	}
}

/*
Later in some other method...
Vec2 v1 = new Vec2(VecType.Position,12,45);
Vec2 v2 = new Vec2(VecType.Velocity,55,34);

v1.addTo(v2);  // This will compile fine but fail at run time.
*/

Type refinement (and similar structures) w/type erasure sucks. A handy thing is abstract T self(); which is defined as return this; in all concrete classes, then you can chain by return self(); instead of return this;. HotSpot is an aggressive inliner so the performance hit (should be) zero.

Likewise for a create method.

Yes, my initial reaction was greatly tempered by the limitations of type erasure. Would you mind explaining a bit more about what you are talking about with abstract T self(); ?

EDIT: I did some googling and found this article which explains things. Google is amazing. :slight_smile:

Yeap, from a quick skim…that’s the idea. Let me semi-rework your example into a self-referential type construction. (NOTE: Personally I’d never have different types for vectors…in fact I go the opposite way and attempt to make as many mathematical type compatible as possible. Be that as it may, a 2D vector is a 2D vector…everything else is what you consider it to represent. OK, enough preaching.)



public abstract class BaseTuple2f<T extends BaseTuple2f<T>>
{
  public float x,y;  // yeah, the public is on purpose...style call here.
  
  abstract T create();
  abstract T self();
  
  public final T set(float x, float y)
  {
    this.x = x;
    this.y = y;
    return self();
  }
  
  // allows assignment from any subclass
  public final T convert(BaseTuple2f<?> v)
  {
    x = v.x;
    y = v.y;
    return self();
  }
  
  // can only be assigned by the same class of this.
  public final T set(T t)
  {
    x = t.x;
    y = t.y;
    return self();
  }
  
  public final T addTo(T t)
  {
    x += t.x;
    y += t.y;
    return self();
  }
  
  // etc, etc.
}



// once you reach a class that isn't genericified, it might as well
// be final, as the 'T' of any derived class won't match.
public final class DirVec2f extends BaseVec2f<DirVec2f>
{
  // This class is all boilerplate to make a concrete version
  
  public DirVec2f() {}
  
  public DirVec2f(float x, float y) { set(x,y); }
  
  @Override public final DirVec2f create() { return new DirVec2f(); }

  @Override public final DirVec2f self() { return this; }
}

// just a boilerplate copy of DirVec2f to make distinct types.
public final class PosVec2f extends BaseVec2f<PosVec2f>
{ 
  public PosVec2f() {}
  
  public PosVec2f(float x, float y) { set(x,y); }
  
  @Override public final PosVec2f create() { return new PosVec2f(); }

  @Override public final PosVec2f self() { return this; }
}


  public static void main() {
    DirVec2f da = new DirVec2f();
    DirVec2f db = new DirVec2f();
    PosVec2f pa = new PosVec2f();
    PosVec2f pb = new PosVec2f();
    
    da.set(2,3);
    db.set(da);       // legal
   //pa.set(da);      // illegal
    pa.convert(da);   // legal
    pb.convert(db);
    da.addTo(db);
   //da.addTo(pa);    // illegal    
  }


Some rough thoughts:
To me, this is exposing and copying internals into static compile time clutter.
Even without phantom types, using generics easily leads to an unreadable mess.
At which number of phantom types should we stop ? You start with one, might be fine. Later on, you need more and to follow the existing style, add more generics parameter. Modifying these constraints could break a lot of classes.

SQL example:
Always use prepared statements to feed the database with user input.

LevelBuilder:
As user of such I wouldn’t like to deal with different types only for its internal state. And I don’t like to specialize every reference anywhere in the whole codebase with it.
A typical parameter for a level builder might be the type of levels it builds.
I don’t see the benefit from restricting addAssets to only one call. Assets could be loaded and cleared dynamically at runtime and the asset parameters must be checked anyway for validity and duplicates.

It’s not heroin, you stop when you have enough. You are aware that if you wanted to you could encode numbers as distinct types for each integer, no? Yet no one is compelled to do so simply because the capability exists.