Certainly; But please do consult https://en.wikipedia.org/wiki/Dependent_type for a more elaborate review.
If we consider the question “Can X depend on X?” where X = {values, types}, and categorize languages by that then we get the following questions:
- Can values depend on values?
- If yes, we get the simply typed lambda calculus (
λ→)
- Can values depend on types?
- If yes, we get System F, 2nd order lambda calculus (
λ2), or just parametric polymorphism / generics, and we can encode: fn id<T>(x: T) -> T { x }
- Can types depend on types?
- If yes, we get
λω or type operators where you can encode type Foo<X> = Vec<Vec<T>>; and specifically to rust, you can encode type level functions such as trait TypeFn<X> { type Y }.
- Can types depend on values?
- If yes, then you get (value) “dependent types”.
So, what (value) dependent types such as in languages like Idris, Agda, Coq, F*, etc. allow is for types to be dependent on values. And so you could encode things such as (with Agda notation):
repeat : {A : Set} // Quantify the type of elements.
-> (x : A) // The element x : A we wish to repeat.
-> (n : Nat) // The number of elements to repeat.
// Notice how Vec a n depends on the input n passed!
-> Vec A n // A list of exactly n As.
append : {A : Set}
-> {n m : Nat}
// Concatenating a list of n elements and one with m gives you n + m elements.
-> Vec A n -> Vec A m -> Vec A (n + m)
index : {A : Set}
-> {n : Nat}
-> {prfLess : idx < n} // Proof that: idx < n
-> (idx: Nat) // The index we wish to extract.
-> Vec A n
-> A // We know that idx < n, so we can safely get an A out always.
This roughly corresponds to the following with pseudo-Rust notation:
fn repeat<A>(x: A, n: usize) -> Vec<A, n> {..}
fn append<A, n: usize, m: usize>(a: Vec<A, n>, b: Vec<A, m>) -> Vec<A, {n + m}> {..}
fn index<A, n: usize>(idx: usize, vec: Vec<A, n>) -> A where idx < n {..}
Thus far, we’ve only considered what are called pi-types, that is, types of the form: Π (x: A) B(x).
But there are also so called sigma-types, or dependent sum types of the form Σ(x: A) B(x). With these sigma types, you can encode things such as (pseudo-Rust, so I’m quite heavily making things up):
struct AnyLenVec<A> {
n: usize,
v: Vec<A, n> // Notice how we are referring to the field `n`!
}
fn index_gvec<A>(idx: usize, vec: AnyLenVec<A>) -> Option<A> {
if idx < vec.n {
Some(index(idx, vec))
} else {
None
}
}
Finally, I’d like to leave a few assorted resources on dependent typing:
Yes, I think you are.
I will agree to it being limited - and more so in the RFC than what const generics could be. You can still build some pretty powerful abstractions with it (cf. the fixed size iterators). The “only” difference, from a user’s perspective, between constant and run-time dependent types is that the former does not allow types to be parameterized by run-time values. So the former allows you to encode Π (const x: A) B x while the latter allows you to encode Π (x: A) B x. This does of course translate into large differences in expressiveness.
I was only comparing the complexity of dependent typing relative to the gain in expressiveness, not the the complexity of dependent typing in absolute terms (which of course will be complicated), wherefore I think it is entirely true wrt. the ratio between power and complexity. I was not referring to complexity in understanding at all.