One of the things that I like about types in programming languages is that they're there to help you (although sometimes feels the opposite). The more things you can check at compile time, the less you need to do during tests (if you need to do it at all!).
One of those techniques are Phantom Types, and although well known in advanced languages (Haskell, Scala, OCaml, etc.) it seems to be relativelly unknown in Java despite the fact it can be perfectly used.
Since Java 1.5, we have parametric types (generics). Once you have that, you can add types parameters to other types...
public class MyTpye <TypeParameter> { ....
Since Java 1.5, we have parametric types (generics). Once you have that, you can add types parameters to other types...
public class MyTpye <TypeParameter> { ....
you can have methods with generic parameters
public <T>
.... do something with p ...
}
or require specific type parameters on your methods (called a generic type invocation ).
public Result doSomethingStringy( MyType<String> p){
.... do something with p ...
}
A ghost in the (type) machine
So, what are phantom types?. Now, normally when you require a specific type in the parameter you use it in the method body (say, when you have sum(List<Integer> xs) you'll use the fact that you have a list of Integers to sum them) , but what happen if you don't? Now you have a parameter type that never appears in the body, although is there, requiring that specific parameter in the type and preventing compilation if doesn't match. That's a phantom type :)
A simple example
Here an example: let's model a plane that can be either flying or landed and a takeOff and land methods that can only be applied to landed and flying planes respectively.
First, let's define the different flight status as marker interfaces:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface FlightStatus { | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface Flying extends FlightStatus{ | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface Landed extends FlightStatus { | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Plane<Status extends FlightStatus> { | |
private Plane(){ | |
// blah blah blah | |
} | |
public Plane(Plane<? extends FlightStatus > p){ | |
//copy whatever info we need | |
} | |
public static Plane<Landed> newPlane(){ | |
return new Plane<Landed>(); | |
} | |
} |
And finally, our flight controller class with the takeOff and land methods:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class AirTrafficController { | |
public static Plane<Landed> land(Plane<Flying> p) { | |
return new Plane<Landed>(p); | |
} | |
public static Plane<Flying> takeOff(Plane<Landed> p) { | |
return new Plane<Flying>(p); | |
} | |
public static void main(String[] args){ | |
Plane<Landed> p=Plane.newPlane(); | |
Plane<Flying> fly=takeOff(p); | |
Plane<Landed> land=land(fly); | |
//doesn't compile: | |
//Plane<Landed> reallyLanded=land(land); | |
//Plane<Flying> reallyFlying=takeOff(fly); | |
} | |
} |
The interesting part is in the land and takeOff methods. For example, in the land method, we require a Plane<Flying> but we just return a landing plane, (without using the Flying interface in the body). That's how we enforce that a plane must be flying to be able to land.
What I wanted to show with this is pretty silly example is how you can use phantom types to enforce some rules.
What are they good for?What I wanted to show with this is pretty silly example is how you can use phantom types to enforce some rules.
Ok, now you have a way to require at compile type certain parameter on a type, what can you use it for?
Turns out it you can use to enforce many constraints:
Enforce a particular state: in the previous example, we used phantom types to require a specific state in a method (flying/landing in this case). In the same way we can require a connection to be open when we do a query or close it:
public ResultSet execute( Connection<Open> c, Query q) ....
public void close( Connection<Open> c) ....
(but is not safe, will work a better example)
You can also use it for safer Ids:
Usually Ids are Int or Strings, and is very easy to mix them up, e.g. in buy(String productId, String customerId) you can swap the Ids by mistake and you can get subtle bugs (the productId might match a customerId). With phantom types you can define an Id class parametrized by the entity and you get buy(Id<Product> productId, Id<Customer> customerId) and you'll get a compiler error if you pass a product id where you expect a customer id.
The full example (in Scala):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
trait Entity | |
trait User extends Entity | |
trait Product extends Entity | |
case class Id[T<:Entity](id:String) | |
def buy(pId:Id[Product],uId:Id[User])="Bought product %s for user %s".format(pId.id,uId.id) | |
val pId=new Id[Product]("1") | |
val uId=new Id[User]("2") | |
/* | |
scala> buy(pId,uId) | |
res2: String = Bought product 1 for user 2 | |
scala> buy(uId,pId) | |
<console>:16: error: type mismatch; | |
found : Id[User] | |
required: Id[Product] | |
buy(uId,pId) | |
^ | |
*/ |
Go ahead and Type!
As you see, phantom types are pretty straightforward and gives you more expressive power (in particular, will allow you to get more mileage from Java's type system). I hope they get more use in Java...
Some useful links:
https://michid.wordpress.com/2008/08/13/type-safe-builder-pattern-in-java/
http://maniagnosis.crsr.net/2010/06/phantom-types-in-java.html
http://www.haskell.org/haskellwiki/Phantom_type
http://en.wikipedia.org/wiki/Phantom_types
https://ocaml.janestreet.com/?q=node/11
http://stackoverflow.com/questions/5881301/implementing-phantom-types-in-c-sharp
Edit:
There's an interesting discussion in reddit
Note:
The connection example is a misleading, as you'll still can hold a reference to the open connection and attempt stuff, I will work a better example.
https://michid.wordpress.com/2008/08/13/type-safe-builder-pattern-in-java/
http://maniagnosis.crsr.net/2010/06/phantom-types-in-java.html
http://www.haskell.org/haskellwiki/Phantom_type
http://en.wikipedia.org/wiki/Phantom_types
https://ocaml.janestreet.com/?q=node/11
http://stackoverflow.com/questions/5881301/implementing-phantom-types-in-c-sharp
Edit:
There's an interesting discussion in reddit
Note:
The connection example is a misleading, as you'll still can hold a reference to the open connection and attempt stuff, I will work a better example.