Can someone explain dependent typing to me? I have little experience in Haskell, Cayenne, Epigram, or other functional languages, so the simpler of terms you can use, the more I will appreciate it!
-
176Well, the article opens with lambda cubes, which sound like some type of sheep meat to me. Then it goes on to discuss λΠ2 systems, and as I don't speak alien I skipped that section. Then I read about the calculus of inductive constructions, which incidentally seems to have little to do with calculus, heat transfer, or construction. After giving a language comparison table, the article ends, and I am left more confused than when I got to the page. – Nick Feb 19 '12 at 05:50
-
6@Nick That’s a general problem with Wikipedia. I saw your comment a few years ago, and I’ve remembered it since. I’m bookmarking it now. – Daniel H Nov 11 '15 at 00:40
-
the Wikipedia page is actually really nice: https://en.wikipedia.org/wiki/Dependent_type#Formal_definition – Charlie Parker Mar 18 '22 at 17:31
-
see this too https://www.quora.com/What-are-dependent-types – Charlie Parker Mar 18 '22 at 17:55
-
3Wikipedia has no sense of "knowledge dependencies" for its articles, and so no means of notating dependencies, let alone detecting *circular* dependencies. This means that 90% of its math and computer science articles are literally impossible to understand, *ever*, from simply reading the other articles linked in a given article. – iono Aug 19 '22 at 19:46
6 Answers
Consider this: in all decent programming languages you can write functions, e.g.
def f(arg) = result
Here, f
takes a value arg
and computes a value result
. It is a function from values to values.
Now, some languages allow you to define polymorphic (aka generic) values:
def empty<T> = new List<T>()
Here, empty
takes a type T
and computes a value. It is a function from types to values.
Usually, you can also have generic type definitions:
type Matrix<T> = List<List<T>>
This definition takes a type and it returns a type. It can be viewed as a function from types to types.
So much for what ordinary languages offer. A language is called dependently typed if it also offers the 4th possibility, namely defining functions from values to types. Or in other words, parameterizing a type definition over a value:
type BoundedInt(n) = {i:Int | i<=n}
Some mainstream languages have some fake form of this that is not to be confused. E.g. in C++, templates can take values as parameters, but they have to be compile-time constants when applied. Not so in a truly dependently-typed language. For example, I could use the type above like this:
def min(i : Int, j : Int) : BoundedInt(j) =
if i < j then i else j
Here, the function's result type depends on the actual argument value j
, thus the terminology.

- 34,518
- 3
- 61
- 72
-
Isn't the `BoundedInt` example actually a Refinement Type, though? That's 'pretty close' but not exactly the kind of 'dependent types' that, e.g., Idris mentions first in a tutorial about dep.typing. – Noein Jun 29 '15 at 22:32
-
9@Noein, refinement types indeed are a simple form of dependent types. – Andreas Rossberg Jun 30 '15 at 18:00
-
I can see it being kind of nice and a way of letting the programmer know that i has to be less than j. But why does it really make that much of a difference vs just checking "if i>j {throw error}"? This kind of type can't be checked at compile-time so effectively it's doing the same thing and I'm just not sure what the advantage is. – mczarnek Sep 10 '21 at 14:18
-
2@mczarnek, these kinds of types *are* checked at compile time. Otherwise they wouldn't be types. – Andreas Rossberg Sep 10 '21 at 18:30
-
@AndreasRossberg I'm not sure I understand - you say C++ templates don't count because they have to be compile-time, yet how can dependant types be checked at compile time if they're dependent on runtime values? – Nabushika Mar 01 '22 at 22:44
-
2@TheAbelo2, in C++, they have to be compile-time _constants_, i.e., their value has to be fully known. They cannot depend on real _variables_, like the function parameters in the `min` example. Yet, certain conditions can still be derived or proved about them at compile-time, see again the `min` example and its result type. – Andreas Rossberg Mar 02 '22 at 06:32
Dependent types enable larger set of logic errors to be eliminated at compile time. To illustrate this consider the following specification on function f
:
Function
f
must take only even integers as input.
Without dependent types you might do something like this:
def f(n: Integer) := {
if n mod 2 != 0 then
throw RuntimeException
else
// do something with n
}
Here the compiler cannot detect if n
is indeed even, that is, from the compiler's perspective the following expression is ok:
f(1) // compiles OK despite being a logic error!
This program would run and then throw exception at runtime, that is, your program has a logic error.
Now, dependent types enable you to be much more expressive and would enable you to write something like this:
def f(n: {n: Integer | n mod 2 == 0}) := {
// do something with n
}
Here n
is of dependent type {n: Integer | n mod 2 == 0}
. It might help to read this out loud as
n
is a member of a set of integers such that each integer is divisible by 2.
In this case the compiler would detect at compile time a logic error where you have passed an odd number to f
and would prevent the program to be executed in the first place:
f(1) // compiler error
Here is an illustrative example using Scala path-dependent types of how we might attempt implementing function f
satisfying such a requirement:
case class Integer(v: Int) {
object IsEven { require(v % 2 == 0) }
object IsOdd { require(v % 2 != 0) }
}
def f(n: Integer)(implicit proof: n.IsEven.type) = {
// do something with n safe in the knowledge it is even
}
val `42` = Integer(42)
implicit val proof42IsEven = `42`.IsEven
val `1` = Integer(1)
implicit val proof1IsOdd = `1`.IsOdd
f(`42`) // OK
f(`1`) // compile-time error
The key is to notice how value n
appears in the type of value proof
namely n.IsEven.type
:
def f(n: Integer)(implicit proof: n.IsEven.type)
^ ^
| |
value value
We say type n.IsEven.type
depends on the value n
hence the term dependent-types.
As a further example let us define a dependently typed function where the return type will depend on the value argument.
Using Scala 3 facilities, consider the following heterogeneous list which maintains the precise type of each of its elements (as opposed to deducing a common least upper bound)
val hlist: (Int, List[Int], String) = 42 *: List(42) *: "foo" *: Tuple()
The objective is that indexing should not lose any compile-time type information, for example, after indexing the third element the compiler should know it is exactly a String
val i: Int = index(hlist)(0) // type Int depends on value 0
val l: List[Int] = index(hlist)(1) // type List[Int] depends on value 1
val s: String = index(hlist)(2) // type String depends on value 2
Here is the signature of dependently typed function index
type DTF = [L <: Tuple] => L => (idx: Int) => Elem[L, idx.type]
| |
value return type depends on value
or in other words
for all heterogeneous lists of type
L
, and for all (value) indicesidx
of typeInt
, the return type isElem[L, idx.type]
where again we note how the return type depends on the value idx
.
Here is the full implementation for reference, which makes use of literal-based singleton types, Peano implementation of integers at type-level, match types, and dependent functions types, however the exact details on how this Scala-specific implementation works are not important for the purposes of this answer which is mearly trying to illustrate two key concepts regarding dependent types
- a type can depend on a value
- which allows a wider set of errors to be eliminated at compile-time
// Bring in scope Peano numbers which are integers lifted to type-level
import compiletime.ops.int._
// Match type which reduces to the exact type of an HList element at index IDX
type Elem[L <: Tuple, IDX <: Int] = L match {
case head *: tail =>
IDX match {
case 0 => head
case S[nextIdx] => Elem[tail, nextIdx]
}
}
// type of dependently typed function index
type DTF = [L <: Tuple] => L => (idx: Int) => Elem[L, idx.type]
// implementation of DTF index
val index: DTF = [L <: Tuple] => (hlist: L) => (idx: Int) => {
hlist.productElement(idx).asInstanceOf[Elem[L, idx.type]]
}
Given dependent type DFT
compiler is now able to catch at compile-time the following error
val a: String = (42 :: "foo" :: Nil)(0).asInstanceOf[String] // run-time error
val b: String = index(42 *: "foo" *: Tuple())(0) // compile-time error

- 47,285
- 6
- 56
- 98
-
13How does it deals with random value? For example, will `f(random())` result in compile error? – Wong Jia Hau Oct 02 '18 at 09:39
-
10Applying `f` to some expression would require the compiler (with or without your help) to provide that the expression is always even, and no such proof exists for `random()` (since it may in fact be odd), therefore `f(random())` would fail to compile. – Matthijs Dec 17 '18 at 07:22
-
3-1. The code shown here illustrates refinement typing, which is related, but not identical to dependent typing. In fact, refinement typing is less expressive than dependent typing. – Xwtek Mar 02 '21 at 05:36
-
1@Xwtek Thank you for feedback. Please see edited answer which hopeful now provides example of [Pi types in Scala](https://stackoverflow.com/a/12937819/5205022). – Mario Galic Apr 25 '21 at 20:33
-
1@MarioGalic: But wouldn't the presence of dependent types (and even refinement types) in a language require the evaluation at compilation time of many expressions that otherwise would get evaluated at run time, thus significantly increasing compilation time? – Alex M. Jul 14 '21 at 16:27
-
1@AlexM., yes, typically compile times go up as it takes work to check the proofs. This is typical as type systems get stronger (a Rust program takes longer to compile than an equivalent Python program). This compile time duration vs runtime safety tradeoff suggests different programs should receive different amounts of compile time checking and safety. High assurance code (where e.g. security and physical safety are critical) takes longer to develop and compile and always has done. – Alex Jan 08 '23 at 12:32
If you happen to know C++ it's easy to provide a motivating example:
Let's say we have some container type and two instances thereof
typedef std::map<int,int> IIMap;
IIMap foo;
IIMap bar;
and consider this code fragment (you may assume foo is non-empty):
IIMap::iterator i = foo.begin();
bar.erase(i);
This is obvious garbage (and probably corrupts the data structures), but it'll type-check just fine since "iterator into foo" and "iterator into bar" are the same type, IIMap::iterator
, even though they are wholly incompatible semantically.
The issue is that an iterator type shouldn't depend just on the container type but in fact on the container object, i.e. it ought to be a "non-static member type":
foo.iterator i = foo.begin();
bar.erase(i); // ERROR: bar.iterator argument expected
Such a feature, the ability to express a type (foo.iterator) which depends on a term (foo), is exactly what dependent typing means.
The reason you don't often see this feature is because it opens up a big can of worms: you suddenly end up in situations where, to check at compile-time whether two types are the same, you end up having to prove two expressions are equivalent (will always yield the same value at runtime). As a result, if you compare wikipedia's list of dependently typed languages with its list of theorem provers you may notice a suspicious similarity. ;-)

- 704
- 7
- 8
Quoting the book Types and Programming Languages (30.5):
Much of this book has been concerned with formalizing abstraction mechanisms of various sorts. In the simply typed lambda-calculus, we formalized the operation of taking a term and abstracting out a subterm, yielding a function that can later be instantiated by applying it to different terms. In System
F
, we considered the operation of taking a term and abstracting out a type, yielding a term that can be instantiated by applying it to various types. Inλω
, we recapitulated the mechanisms of the simply typed lambda-calculus “one level up,” taking a type and abstracting out a subexpression to obtain a type operator that can later be instantiated by applying it to different types. A convenient way of thinking of all these forms of abstraction is in terms of families of expressions, indexed by other expressions. An ordinary lambda abstractionλx:T1.t2
is a family of terms[x -> s]t1
indexed by termss
. Similarly, a type abstractionλX::K1.t2
is a family of terms indexed by types, and a type operator is a family of types indexed by types.
λx:T1.t2
family of terms indexed by terms
λX::K1.t2
family of terms indexed by types
λX::K1.T2
family of types indexed by typesLooking at this list, it is clear that there is one possibility that we have not considered yet: families of types indexed by terms. This form of abstraction has also been studied extensively, under the rubric of dependent types.

- 37,139
- 8
- 58
- 74
I will try to provide an answer that goes straight to the chase.
A dependent type is the label used to indicate that the output's type (i.e. the type of the co-domain) depends on the actual input value/argument passed to the (dependent) function. e.g. F:forall a:A, Y(A)
means the input type of F
is A
and that depending on the specific value of a
the output type will be Y(a)
. So the output type depends on the input argument.
For a normal function we usually write f: A -> B
which means that function f
, any input of type A
(informally \forall a \in A
or a:A
) returns some element of type B
(informally some a \in B
or b:A
). That is a "normal" function type. However, a dependent type indicates that the output type of the (dependent) function F
depends on the argument. Similarly, the output type is indexed (i.e. "depends") by the value of the argument. E.g. the notation I like (that nobody else uses afaik) is F: a:A -> Y(a)
or the common notation F: forall a:A, Y(a)
. These notations simply say F
takes some element a
in A
(i.e. of type A
) and returns some element y
of type Y(a)
(or if you like index sets more use Y_a
). It just means that the output type of F
changes depending on the input value a
to F
.
A common example is a function F: forall n:Nat, Vector n
which explicitly specifies the size of the array returned/outputed. So if you call F(a)
then the output is of type Vector n
which means it can only return a vector of size n
denoted F(a):Y(a)
. As you can guess, if you always somehow guarantee this return type is respected, you make it harder to make out of bound errors (which is good for security).
I actually really like the Wikipedia article section on product types and think it's very thorough. If some part of it doesn't make sense of that small section ask me here I'm happy to clarify in the comments section https://en.wikipedia.org/wiki/Dependent_type#Formal_definition. Hope to explain in more detail what the article says in the future -- especially the what a product type means and it's relation to Cartesian products.

- 5,884
- 57
- 198
- 323
An expression is said to be a dependent type if its type depends on a value.
Lets take C++. The expression "int" is of integer type. Its type doesn't depend on anything and is always the same. Lets define another type called "int(x)", where x is a number. Lets define the purpose of the type int(x) as helping us to have an integer with only "x" number of digits. Thus, the type of int(x) depends on the value of x, where the value of x can be determined at runtime. For example, we can have a function that contains a statement that has the expression "int(x)" where the value of x is passed as an argument to the function. What we are saying is that, for example, int(2) and int(3) are of different data types, i.e. how they behave during computation is different.

- 1,147
- 1
- 8
- 14