The Problem
In haskell we can write:
data Maybe a = Just a | Nothing
And similarly in rust we can write:
enum Maybe<T> {
Just(T),
Nothing
}
But, in haskell the above example is just syntactic sugar for:
data Maybe a where
Just :: a -> Maybe a
None :: Maybe a
Which isn't the case in rust.
This generalization of data constructors lets us do things like creating a vector with a type-level length:
data Zero
data Succ a
data Vector l a where
Nil :: Vector Zero a
Cons :: a -> Vector l a -> Vector (Succ l) a
-- length is included as part of the type!
x :: Vector (Suc (Suc (Suc Zero))) Int
x = 1 `Cons` 2 `Cons` 3 `Cons` Nil
This is somewhat possible in rust using private traits:
// Makes it possible to use the `Vector` trait outside of the
// module where its defined without being able to implement it.
trait PrivateVector<T, L> {}
pub trait Vector<T, L>: PrivateVector<T, L> {}
impl<T, L, V: PrivateVector<T, L>> Vector<T, L> for V {}
pub struct Zero;
pub struct Succ<T>(std::marker::PhantomData<T>);
pub struct Nil {}
impl<T> PrivateVector<T, Zero> for Nil {}
pub struct Cons<T, L, V: Vector<T, L>>(T, std::marker::PhantomData<L>, V);
impl<T, L, V: Vector<T, L>> PrivateVector<T, Succ<L>> for Cons<T, L, V> {}
But, besides it being much more verbose, this method is much more limiting because it doesn't allow us to do pattern matching:
fn some_fn<T, L, V: Vector<T, L>>(vector: V) {
match vector {
// Rust thinks this is a variable named `Nil`
Nil => (),
// mismatched types: expected type parameter `V`
// found struct `Cons<_, _, _>`
Cons(_, _, _) => (),
}
}
NOTE: you can kind of achieve pattern matching with traits but it would essentially require you to include multiple constraints for every function including all the traits you need to pattern match that specific one.
The Suggestion
There are two possibilities here in my eyes, one is to allow pattern matching on generic values:
fn some_fn<T>(value: T) {
match value {
Option::Some(value) => (),
Result::Err(value) => (),
// default case is required because `value` could literlly be anything
_ => ()
}
}
And then we could use that for the specific case of GADTs:
fn some_fn<T, L, V: Vector<T, L>>(vector: V) {
match vector {
Nil => (),
Cons(_, _, _) => (),
// the compiler recognizes `value` can be a finite amount of things
// so a default case isn't required.
}
}
Which is unlikely given rust's low level nature.
Or, alternatively, we can extend the enum syntax to make haskell-style data constructors possible:
struct Zero;
struct Succ<T>(std::mem::PhantomData<T>);
// proposed syntax
enum Vector<T, L> {
// notice generics have to be specified for each varient seperately
Nil<T> -> Vector<T, Zero>,
Cons<T,L>(T, Box<Vector<T,L>>) -> Vector<T, Succ<L>>
}
Pattern matching is then done as usual:
// no need to deal with dynamic traits, which is a bonus too
fn some_fn<T, L>(vector: Vector<T,L>) {
match vector {
// Here it should be inferred that L=Zero
Nil => (),
// Here it should be inferred that L=Succ<_>
Cons(_,_) => ()
}
}
This syntax can also finally enable a safe implementation of a type equality type:
enum TypeEq<T,K> {
Refl<T> -> TypeEq<T,T>
}
impl<T,K> TypeEq<T,K> {
fn cast(&self, value: T) -> K {
match &self {
// it is inferred that this case can only be reached when T=K
Refl => value
}
}
}
Conclusion
GADTs (which is what haskell's data constructors are) are a really powerful tool for expressing types that more thoroughly check our values out of the box, without the need for the parse pattern or complex traits that make dealing with the actual values that use them a pain.
What do you think? Is this feature too complicated for a compiled language? Should this be an entirely new data type, separately from enum
? Is there an already a way to do this using the existing type system that I missed?