Phantom Types in Rust
bluishcoder.co.nz
bluishcoder.co.nz
Phantom types along with quantified type erasure allow the compiler to force these impure regions to be contained. They disallow sharing of variables or results between ST monad invocations statically.
To do so, most operations are polymorphic in the phantom variable. So something like `get (put x box)` is of type `ST s Int` for an `x :: Int`. The `s` corresponds to whichever ST region this action gets run within.
Running ST looks like `runST :: (forall s. ST s a) -> a` which means that the ST action you give to ST must be completely naive in its choice of region. Another way of thinking about it is that the `s` parameter is chosen adversarially by the compiler so that only ST actions which are able to be run in any region are allowed to be run at all.
Sadly! Books and their covers... I'm sure they were good.
I did find the article I was thinking of, but today keyword search has failed me. I was looking for "monotonically increasing timers" and the correct search was "fast now"
http://re-factor.blogspot.com/2011/03/fast-now.html
Chris D, are you here reading comments? Can you bring back your Factor articles?
I've been going through archive.org recovering posts when I can. I do have a bunch of the factor articles in my project that produces a PDF from them: https://github.com/doublec/factor-articles
So I assume the compiler infers the type of `TI(1)` as `T<int>` and `TS(~"Hello")` as `T<~string>` because ...?
What if you defined:
enum T<A> {
TX(int, ~string),
TY(~string, int)
}
What's the inferred type of `TX(1, ~"a")`? let a = TI(1);
let b = TS(~"foo");
let z = concat(a, b);
The reason is that the Rust compiler is trying to work out the types for you. In this case, it sees that concat requires two T<~str>s and infers logically that both a and b must have the type T<~str>... even though one of them was created with TI.If you look at the example with a compile error, you'll notice that the two things being concatenated which result in the error were themselves the results of calls to `plus` and `concatenate`, which are guaranteed to produce T<int> and T<~string>. You could also guide the compiler by providing the types manually
let a : T<int> = TI(1);
let b : T<~str> = TS(~"foo");
/* now this produces a compile-time error */
let z = concat(a, b);
Or by writing wrapper functions over the constructors fn makeTI(i : int) -> T<int> { TI(i) }
fn makeTS(s : ~str) -> T<~str> { TS(s) }
...
let a = makeTI(1);
let b = makeTS(~"foo");
/* this also now produces a compile-time error */
let z = concat(a, b);
Other languages with phantom types use something called Generalized Algebraic Data types (GADTs) which—to handwave a bit—are something like the latter solution, except built directly into the data type constructors (rather than manually writing those extra functions).Say something like:
enum Node<A> { StringNode(Node, Node, ~string) IntNode(Node, Node, int) VoidNode(Node, Node) }
Is there any way of declaring that StringNode gives you Node<~string>, and IntNode gives Node<int>?
data T a where
TI :: Int -> T Int
TS :: String -> T String
which lets you reflect a value up into its type. Something similar but more implicit seems to be going on with that Rust example.Another method is kind of a hack: unify the type variable with an unused argument. This looks like
data T a b = T b -- we want to find a way to choose a
setA :: a -> T a b -> T a b
setA _ t = t -- Just returns the second argument
-- BUT tells the compiler the `a` type unifies
-- with the type of the first argument
> :t setA Int (T "foo")
setA Int (T "foo") :: T Int StringThe cool thing is that all this functionality is accessible through the match() syntax, which exhaustively checks all possible forms the Enum can take, thus making data access completely safe.
This sort of overloading of language happens all over the place in programming and it leads to endless confusion. I find it extremely frustrating but what can I do about it?
Also Enum seems a good name because they represent fixed alternatives that may be (finitely) enumerated (We're saying "Let me enumerate the alternatives that this type may hold."). Whereas "union" seems to speak to its underlying implementation (which may or may not hold) as a tagged, overlapping union. Ie. that usage of union says much less about the intended semantics and is more of a possible implementation detail. Otherwise if you look at the actual meaning of "union" it doesn't seem to fit really. Maybe "sum" might be closer but again that's leaning towards the theoretical, and in that case you'd probably then need to rename struct to become "product" for symmetry.
enum Day {
Monday, Tuesday, Wednesday, ...
}
You can assign values too: enum Color {
Red = 0xff0000,
Green = 0x00ff00,
Blue = 0x0000ff
}