Phantom type
A phantom type parameter appears in a type's declaration and in none of its constructors[1][3]. It carries no data, is erased before run time, and exists only so the type checker can refuse to let two representationally identical values be confused. Leijen and Meijer used phantom types in 1999 to give types to SQL queries built as strings[1]; they are now a standard technique for units, validation states, capabilities and protocol states, often called typestate[6]. The technique depends on abstraction: the type is exported without its constructors, so the only way to obtain a value at a particular phantom type is through the functions the library provides. Fluet and Pucella showed that phantom parameters can also encode subtyping[2]. In Haskell, a phantom parameter must be given the nominal role or coerce can change it freely[5]. Rust marks such parameters with PhantomData[8]. The difference from a GADT is what a pattern match learns.
Example
Lengths in OCaml. Inside the module a length is a float; outside, its unit is part of its type.
(* The parameter 'u appears in no constructor: every length is a float atrun time, and the unit exists only for the type checker. *)module Length : sigtype 'u ttype metrestype feetval metres : float -> metres tval feet : float -> feet tval add : 'u t -> 'u t -> 'u tval to_metres : feet t -> metres tval value : 'u t -> floatend = structtype 'u t = floattype metrestype feetlet metres x = xlet feet x = xlet add = ( +. )let to_metres x = x *. 0.3048let value x = xend
Using it.
open Units
Running it.
3304.8 metres
Adding metres to feet.
open Unitslet total = Length.add (Length.metres 3000.) (Length.feet 1000.)
The compiler's error.
File "units_bad.ml", line 2, characters 45-64:2 | let total = Length.add (Length.metres 3000.) (Length.feet 1000.)^^^^^^^^^^^^^^^^^^^Error: This expression has type Units.Length.feet Units.Length.tbut an expression was expected of typeUnits.Length.metres Units.Length.tType Units.Length.feet is not compatible with type Units.Length.metres
Nothing in the representation distinguishes the two units, and the conversion to_metres is an ordinary multiplication. The guarantee comes from the signature: 'u t is abstract, so outside the module the only lengths are those built by metres and feet.
How it works
A phantom parameter adds distinctions between types that the representation does not make. Three things make it work. The parameter must appear in the type, so that and are different types when . The type must be abstract, so that clients cannot build a value at an arbitrary instantiation. And the library's functions must fix the parameter in their signatures, which is how the invariants are stated: validate : Email Unchecked -> Maybe (Email Checked) is the only producer of checked addresses.
Since the parameter is erased, there is no run-time cost and no run-time check; everything is decided by type checking. The corresponding limitation is that the type checker knows only what the signatures say: a function that could produce a value at the wrong phantom type, because it is inside the abstraction boundary, is trusted.
Typed embedded languages
Leijen and Meijer introduced phantom types to type an embedded query language[1]. Queries are built as SQL strings, but each expression carries the SQL type of its result as a phantom parameter, so the combinators can accept only well-typed operands.
A fragment of a typed SQL builder.
-- Leijen and Meijer's embedded query language: an expression is a string-- of SQL, and the phantom parameter records the SQL type it denotes.newtype Expr a = Expr Stringcolumn :: String -> Expr acolumn = Exprint :: Int -> Expr Intint = Expr . showstr :: String -> Expr Stringstr s = Expr ("'" ++ s ++ "'")(.+.) :: Expr Int -> Expr Int -> Expr IntExpr a .+. Expr b = Expr ("(" ++ a ++ " + " ++ b ++ ")")(.==.) :: Expr a -> Expr a -> Expr BoolExpr a .==. Expr b = Expr ("(" ++ a ++ " = " ++ b ++ ")")(.&&.) :: Expr Bool -> Expr Bool -> Expr BoolExpr a .&&. Expr b = Expr (a ++ " AND " ++ b)where_ :: String -> Expr Bool -> Stringwhere_ table (Expr cond) = "SELECT * FROM " ++ table ++ " WHERE " ++ condage :: Expr Intage = column "age"name :: Expr Stringname = column "name"main :: IO ()main = putStrLn (where_ "people" ((age .+. int 1 .==. int 30) .&&. (name .==. str "Ada")))
Running it.
SELECT * FROM people WHERE ((age + 1) = 30) AND (name = 'Ada')
Comparing an integer column with a string.
import Sqlquery = where_ "people" (age .==. str "thirty")
GHC's error.
sql_bad.hs:2:35: error:* Couldn't match type `[Char]' with `Int'Expected: Expr IntActual: Expr String* In the second argument of `(.==.)', namely `str "thirty"'In the second argument of `where_', namely`(age .==. str "thirty")'In the expression: where_ "people" (age .==. str "thirty")|2 | query = where_ "people" (age .==. str "thirty")| ^^^^^^^^^^^^
Subtyping with phantom types
A phantom parameter that is itself a set of capabilities can encode subtyping. Fluet and Pucella showed how to encode subtyping hierarchies in the parameters of phantom types in ML[2]; in OCaml, polymorphic variants make it direct. A handle's parameter lists the operations it permits, and a function that needs only reading accepts any handle whose list includes `Read.
Read and write capabilities as phantom polymorphic variants.
(* Capabilities as phantom polymorphic variants (Fluet and Pucella):a handle's parameter lists what it may be used for, and subtyping onvariants lets a read-write handle go wherever a read handle is needed. *)module File : sigtype 'cap tval open_rw : string -> [ `Read | `Write ] tval read_only : [> `Read ] t -> [ `Read ] tval read : [> `Read ] t -> stringval write : [> `Write ] t -> string -> unitend = structtype 'cap t = { name : string; contents : Buffer.t }let open_rw name = { name; contents = Buffer.create 16 }(* the same file, retyped: 'cap is phantom, so this is just a copy ofthe record *)let read_only f = { name = f.name; contents = f.contents }let read f = Buffer.contents f.contentslet write f s = Buffer.add_string f.contents send
Using a read-write handle and a read-only view of it.
open Perms
Running it.
started; ok
Writing through the read-only view.
open Permslet () = File.write (File.read_only (File.open_rw "log")) "oops"
The compiler's error.
File "perms_bad.ml", line 2, characters 20-57:2 | let () = File.write (File.read_only (File.open_rw "log")) "oops"^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Error: This expression has type [ `Read ] Perms.File.tbut an expression was expected of type [> `Write ] Perms.File.tThe first variant type does not allow tag(s) `Write
Roles and coercions
Haskell's coerce converts between types with the same representation at no cost, and its safety is tracked by roles[5]. A type parameter that is not used in the representation gets the phantom role, which means coerce may change it to anything, even when the constructor is not exported. For a phantom type used to enforce an invariant, that is a hole.
A validation library that hides its constructor.
module Email (Email, Unchecked, Checked, unchecked, validate, send) wheredata Uncheckeddata Checked-- The constructor is not exported: the only way to get an Email Checked-- is through validate.newtype Email s = Email Stringunchecked :: String -> Email Uncheckedunchecked = Emailvalidate :: Email Unchecked -> Maybe (Email Checked)validate (Email s) = if '@' `elem` s then Just (Email s) else Nothingsend :: Email Checked -> Stringsend (Email s) = "sent to " ++ s
A client that bypasses validate.
import Data.Coerce (coerce)import Emailmain :: IO ()main = doprint (fmap send (validate (unchecked "[email protected]")))print (fmap send (validate (unchecked "nobody")))-- The parameter s has the phantom role, so coerce may change it even-- though the constructor is hidden:putStrLn (send (coerce (unchecked "nobody")))
Running it: an address that failed validation is sent anyway.
Just "sent to [email protected]"Nothingsent to nobody
The fix is a role annotation, type role Email nominal, which makes Email Unchecked and Email Checked non-coercible outside the module.
The same client against the annotated library.
type role Email nominalnewtype Email s = Email String
GHC's error.
roles_safe.hs:10:19: error:* Couldn't match type `Unchecked' with `Checked'arising from a use of `coerce'* In the first argument of `send', namely`(coerce (unchecked "nobody"))'In the first argument of `putStrLn', namely`(send (coerce (unchecked "nobody")))'In a stmt of a 'do' block:putStrLn (send (coerce (unchecked "nobody")))|10 | putStrLn (send (coerce (unchecked "nobody")))| ^^^^^^
Phantom types and GADTs
The difference from a GADT is what happens on the way out. A phantom parameter constrains callers but tells a pattern match nothing: matching on a length's constructor learns no more about its unit than was already known, because every constructor has the same fully polymorphic result type. A GADT constructor fixes its type index, so matching on it recovers the index, which is why a GADT branch can return a value at a type that depends on the constructor and a phantom-typed match cannot[3][4]. GADTs are sometimes called first-class phantom types for this reason[4].
Other uses
Typestate, in which the type of a resource records its protocol state, such as a file being open or closed or a builder having set its required fields, was proposed by Strom and Yemini in 1986[6] and is commonly encoded with phantom parameters, especially in Rust, whose PhantomData marker declares a parameter that the fields do not use[8]. Other uses include tagging identifiers with the kind of entity they refer to, tracking whether a string has been escaped, and tagging indices with the array they are valid for. Units of measure are a common example, although F# builds them into the type system, following Kennedy's dimension types[7], so that units can be multiplied and divided, which phantom parameters alone cannot express.
History
Leijen and Meijer introduced the term in 1999, for the typed query combinators of their embedded compiler for SQL[1]. Hinze's “Fun with phantom types” of 2003 and Cheney and Hinze's “First-class phantom types” described the extension to type indices that pattern matching can recover, which became GADTs[3][4]. Fluet and Pucella gave the encoding of subtyping in 2006[2]. GHC's roles, introduced with coerce in 2014, made the phantom role explicit and added role annotations[5].
see also
- GADTA generalized algebraic data type (GADT) is an algebraic data type whose constructors may each give a more specific result type than the declared one, instead of all sharing one polymorphic return type. Matching on a constructor then tells the type checker what the type parameter was, so a single well-typed eval can return an int for an Add node and a bool for an Eq node, checked at compile time rather than by a run-time tag test. GADTs express typed syntax trees, type representations and equality proofs, typed format strings and length-indexed data. The price is that type inference needs annotations.
- Polymorphic variantA polymorphic variant is a variant whose constructors, called tags and written with a backquote in OCaml, exist independently of any type declaration. Their types are sets of tags with bounds: [> `A] means at least `A, [< `A | `B] means at most those two, and unification works out unions and intersections of those sets using row variables. They let functions share constructors across types, be extended with new cases without editing old code, and encode subtyping, at the cost of longer types and error messages and of typos in open types going unnoticed.
- Dependent typeA dependent type is a type that may mention values, so that a vector's length can appear in its type and a function's result type can be computed from its arguments. Function types generalize to dependent products Π(x : A). B(x), and pairs to dependent sums Σ(x : A). B(x). Because types can express propositions and programs can be proofs, languages with dependent types, such as Agda, Coq (Rocq), Lean and Idris, double as proof assistants. The price is that type checking involves evaluating programs, so these languages require functions to terminate, and type equality is decided up to computation.
- Refinement typeA refinement type is a base type paired with a logical predicate that its values must satisfy, such as {v : Int | v > 0}, the positive integers. Type checking generates verification conditions, implications between predicates, and discharges them with an SMT solver, so a program can be checked against preconditions, postconditions and invariants without the programmer writing proof terms. The predicates are restricted to a decidable logic, typically linear arithmetic with uninterpreted functions, which is what keeps checking automatic. LiquidHaskell, F* and Liquid Types are the best-known systems.
- Deriving strategyA deriving strategy is an explicit choice of how GHC produces a derived type class instance: stock, generated by the compiler from the constructors; newtype, reusing the wrapped type's instance by coercion; anyclass, an empty instance that takes the class's default methods; or via, reusing the instance of another type with the same representation. Several strategies can apply to the same declaration, with different results, so naming the strategy removes the ambiguity. The two coercing strategies are sound only when the class's methods can be coerced, which GHC checks with type roles.
- Value restrictionThe value restriction is the rule, in ML-family languages, that a let-bound expression has its type variables generalized only if it is a syntactic value, such as a function, a constant or a constructor applied to values. An application such as List.map f or ref [] is not a value, so its type variables stay weak and are fixed by their first use. Without the restriction, a reference could be given a polymorphic type and used to store a value at one type and read it at another. OCaml relaxes the rule for type variables that occur only covariantly.
referenced by
further reading
- [1]D. Leijen, E. Meijer, “Domain specific embedded compilers”, 2nd USENIX Conference on Domain-Specific Languages (1999).
- [2]M. Fluet, R. Pucella, “Phantom types and subtyping”, Journal of Functional Programming 16 (2006).
- [3]R. Hinze, “Fun with phantom types”, in J. Gibbons, O. de Moor (eds.), The Fun of Programming, Palgrave Macmillan (2003).
- [4]J. Cheney, R. Hinze, “First-class phantom types”, Technical Report TR2003-1901, Cornell University (2003).
- [5]J. Breitner, R. A. Eisenberg, S. Peyton Jones, S. Weirich, “Safe zero-cost coercions for Haskell”, Journal of Functional Programming 26 (2016).
- [6]R. E. Strom, S. Yemini, “Typestate: a programming language concept for enhancing software reliability”, IEEE Transactions on Software Engineering 12 (1986).
- [7]A. Kennedy, “Dimension types”, ESOP (1994).
- [8]The Rust Standard Library, std::marker::PhantomData.
last updated