I’m a guy who is somewhat familiar with Haskell who is trying to learn Rust. So I thought I’d try to replicate some cool Haskell functionality in Rust. I would love to hear comments, because I’m trying to learn. I have no sense of Rust aesthetics yet and in particular I have no idea how this interacts with the borrow system. What follows is a pretty rough brain dump

GADTs (Generalized algebraic data types) are an extension in Haskell that allows you to write constrained type signatures for your data constructors. They also change how the type checking of pattern matching is processed.

GADTs are sometimes described/faked by being built by making data types that hold equality/unification constraints. Equality constraints in Haskell like `a ~ Int`

are fairly magical and the Rust compiler does not support them in an obvious way. Maybe this is the next project. Figure out how to fake ’em if one can. I don’t think this is promising though, because faking them will be a little wonky, and then GADTs are a little wonky on top of that. See https://docs.rs/refl/0.1.2/refl/ So we’ll go another (related) road.

This is roughly what GADTs look like in Haskell.

1 2 3 |
data MyGadt a where TBool :: Bool -> MyGadt Bool TInt :: Int -> MyGadt Int |

And here is one style of encoding using smart constructors and a typeclass for elimination (pattern matching is replicated as a function that takes callbacks for the data held in the different cases). Regular functions can have a restricted type signature than the most general one their implementation implies. The reason to use a typeclass is so that we can write the eliminator as returning the same type that the GADT supplies. There isn’t an explicit equality constraint. A kind of Leibnitz equality

1 |
(forall f. f a -> f b) |

is hiding in the eliminator. The Leibnitz equality can be used in place of (~) constraints at some manual cost. http://code.slipthrough.net/2016/08/10/approximating-gadts-in-purescript/

https://jesper.sikanda.be/files/leibniz-equality.pdf

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
data MyGadt2 a = TBool2 Bool | TInt2 Int -- smart constructors. Hide the original constructors tInt :: Int -> MyGadt2 Int tInt = TInt tBool :: Bool -> MyGadt2 Bool tBool = TBool -- gadt eliminator class MyGadtElim a where elimMyGadt :: forall f. MyGadt2 a -> (Bool -> f Bool) -> (Int -> f Int) -> f a instance MyGadtElim Int where elimMyGadt (TInt2 x) fb fi = fi x elimMyGadt _ _ _ = error "How did TBool2 get type MyGadt2 Int?" instance MyGadtElim Bool where elimMyGadt (TBool2 x) fb fi = fb x |

The

1 |
forall f :: * -> * |

is a problem for Rust. Rust does not have higher kinded types, although they can be faked to some degree. https://gist.github.com/CMCDragonkai/a5638f50c87d49f815b8 There are murmurs of Associated Type Constructors / GATs , whatever those are , that help ease the pain, but I’m pretty sure they are not implemented anywhere yet.

I’m going to do something related, a defunctionalization of the higher kinded types. We make an application trait, that will apply the given type function tag to the argument. What I’m doing is very similar to what happens in the singletons library, so we may be getting some things for free.

https://typesandkinds.wordpress.com/2013/04/01/defunctionalization-for-the-win/

1 |
trait App1<A> { type T;} |

Then in order to define a new typelevel function rip out a quick tag type and an App impl.

1 2 3 4 |
struct F1 {} impl<A> App1<A> for F1{ type T = Vec<Vec<A>>; } |

It might be possible to sugar this up with a macro. It may also be possible to write typelevel functions in a point free style without defining new function tag names. The combinators Id, Comp, Par, Fst, Snd, Dup, Const are all reasonably definable and fairly clear for small functions. Also the combinator S if you want to talk SKI combinatory calculus, which is unfit for humans. https://en.wikipedia.org/wiki/SKI_combinator_calculus For currying, I used a number for how many arguments are left to be applied (I’m not sure I’ve been entirely consistent with these numbers). You need to do currying quite manually. It may be better to work with tuplized arguments

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
struct Vec1 {} // number is number of aspplications left. impl<A> App1<A> for Vec1{ type T = Vec<A>; } struct Id(); impl<A> App1<A> for Id{ type T = A; } // partially applied const. // struct Const() // call this next one const1 struct Const1(); struct Const<B>(PhantomData<B>); // should be Const1 impl<A> App1<A> for Const1 { // partial application type T = Const<A>; } impl<A,B> App1<A> for Const<B>{ type T = B; } struct Fst {} impl <A,B> App1<(A,B)> for Fst { type T = A; } struct Snd {} impl <A,B> App1<(A,B)> for Snd { type T = B; } struct Dup{} impl <A> App1<A> for Dup { type T = (A,A); } struct Par2 {} struct Par1<A> (PhantomData<A>); struct Par<A,B> (PhantomData<A> , PhantomData<B> ); impl<F> App1<F> for Par2 { type T = Par1<F>; } impl<F,G> App1<G> for Par1<F> { type T = Par<F,G>; } impl<X,Y,F,G> App1<(X,Y)> for Par<F,G> where F : App1<X>, G : App1<Y> { type T = (F::T, G::T); } // In order to curry, i think I'd have to define a name for every curried form. // combinator calculus Const is K, Id is I, and here is S combinator. Yikes. type I = Id; type K<A> = Const<A>; struct S3{} struct S2<A>(PhantomData<A>); struct S1<A,B>(PhantomData<A>, PhantomData<B>); // struct S<A,B,C>(PhantomData<A>, PhantomData<B>, PhantomData<C>); impl <A,B,C> App1<C> for S1<A,B> where A : App1<C>, B : App1<C>, <A as App1<C>>::T : App1< <B as App1<C>>::T > { type T = < <A as App1<C>>::T as App1< <B as App1<C>>::T > >::T; } struct Comp2(); struct Comp1<F> (PhantomData<F>); struct Comp<F,G> (PhantomData<F>, PhantomData<G>); impl<F,G, X> App1<X> for Comp<F,G> where G : App1<X>, F : App1<<G as App1<X>>::T> { type T = <F as App1<<G as App1<X>>::T>>::T; } |

Anyway, the following is a translation of the above Haskell (well, I didn’t wrap an actual i64 or bool in there but I could have *I think*). You need to hide the actual constructors labeled INTERNAL in a user inaccessible module.

1 2 3 4 5 6 7 8 9 10 11 12 13 |
enum Gadt<A> { TIntINTERNAL(PhantomData<A>), TBoolINTERNAL(PhantomData<A>) } // then build the specialzied constructors that fn TBool() -> Gadt<bool>{ Gadt::TBoolINTERNAL(PhantomData) } fn TInt() -> Gadt<i64>{ Gadt::TIntINTERNAL(PhantomData) } |

The smart constructors put the right type in the parameter spot

Then pattern matching is a custom trait per gadtified type. Is it possible to unify the different elimination traits that will come up into a single Elim trait? I’m 50-50 about whether this is possible. What we’re doing is a kind of fancy `map_or_else`

if that helps you.

https://doc.rust-lang.org/std/option/enum.Option.html#method.map_or_else

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
trait GadtElim { type Inner; fn gadtElim<F>(&self, case1 : <F as App1<bool>>::T , case2 : <F as App1<i64>>::T ) -> <F as App1<Self::Inner>>::T where F : App1<bool>, F : App1<i64>, F : App1<Self::Inner>; } impl GadtElim for Gadt<i64> { type Inner = i64; fn gadtElim<F>(&self, case1 : <F as App1<bool>>::T , case2 : <F as App1<i64>>::T ) -> <F as App1<Self::Inner>>::T where F : App1<bool>, F : App1<i64>, F : App1<Self::Inner>{ match self{ Gadt::TIntINTERNAL(PhantomData) => case2, Gadt::TBoolINTERNAL(PhantomData) => panic!("Somehow TBool has type Gadt<i64>")// Will never be reached though. god willing } } } impl GadtElim for Gadt<bool> { type Inner = bool; fn gadtElim<F>(&self, case1 : <F as App1<bool>>::T , case2 : <F as App1<i64>>::T ) -> <F as App1<Self::Inner>>::T where F : App1<bool>, F : App1<i64>, F : App1<Self::Inner>{ match self{ Gadt::TIntINTERNAL(PhantomData) => panic!("Somehow TInt has type Gadt<bool>"), Gadt::TBoolINTERNAL(PhantomData) => case1 // Will never be reached though. god willing } } } |

Usage. You have to explicitly pass the return type function to the eliminator. No inference is done for you. It’s like Coq’s match but worse. BTW the `dbg!`

macro is the greatest thing on earth. Well done, Rust.

1 2 3 4 5 6 7 8 |
let z = TInt().gadtElim::<Id>(true , 34); let z2 = TBool().gadtElim::<Id>(true , 34); dbg!(z); dbg!(z2); struct F7 {} // You need to do this. Kind of sucks. macroify? App!(F<A> = Vec<A>) impl<A> App1<A> for F7 { type T = Vec<A>; } dbg!(TInt().gadtElim::<F7>(vec!(true,false) , vec!(34,45,4,3,46))); |

You can make helpers that don’t require explicit types to be given

1 2 3 |
fn gadtRec<A>(x : impl GadtElim, case1 : A, case2 : A) -> A { x.gadtElim::<Const<A>>(case1 , case2) } |

One could also make an `Eq a b`

type with Refl similarly. Then we need typelevel function tags that take two type parameter. Which, with currying or tupling, we may already have.

Questions:

Is this even good? Or is it a road of nightmares? Is this even emulating GADTs or am I just playing phantom type games?

We aren’t at full gadt. We don’t have existential types. Rust has some kind of existential story evolving (already there?), but the state of it is confusing to me. Something to play with. Higher rank functions would help?

Are overlapping typeclasses a problem in Rust?

Again, I have given nearly zero thought to borrowing and how it interacts with this. I’m a Rust n00b. I should think about it. Different eliminators based on whether you own or are borrowing?.

How much of singleton style dependent types do we get from this? It feels like we have already paid the cost of defunctionalizing. http://hackage.haskell.org/package/singletons

My current playground for this is at https://github.com/philzook58/typo