Foundations of path-dependent types

Nada Amin, Tiark Rompf, Martin Odersky
2014 SIGPLAN notices  
A scalable programming language is one in which the same concepts can describe small as well as large parts. Towards this goal, Scala unifies concepts from object and module systems. An essential ingredient of this unification is the concept of objects with type members, which can be referenced through path-dependent types. Unfortunately, pathdependent types are not well-understood, and have been a roadblock in grounding the Scala type system on firm theory. We study several calculi for
more » ... endent types. We present µDOT which captures the essence -DOT stands for Dependent Object Types. We explore the design space bottom-up, teasing apart inherent from accidental complexities, while fully mechanizing our models at each step. Even in this simple setting, many interesting patterns arise from the interaction of structural and nominal features. Whereas our simple calculus enjoys many desirable and intuitive properties, we demonstrate that the theory gets much more complicated once we add another Scala feature, type refinement, or extend the subtyping relation to a lattice. We discuss possible remedies and trade-offs in modeling type systems for Scala-like languages. [Copyright notice will appear here once 'preprint' option is removed.] Motivating Example in Scala The key use case for path-dependent types is to model nominality through abstract type members, as we show through an example in Scala inspired by [15] . Nominality through Abstract Type Members An animal eats food of a certain type that depends on the animal. We model an animal with a trait Animal that has an abstract type member type Food. The variable a denotes the self (i.e. this) object, in the scope defining the trait Animal. So we can refer to the abstract type member type Food as a type through a type selection: a.Fooda.Food is a path-dependent type, in general, a chain, starting with an immutable variable, of immutable field selections, ending with a type selection. Notice that the type selection a.Food can appear both covariantly, as in the method def gets, where it is the return type, and contravariantly, as in the method def eats, where it is a parameter type. trait Animal { a => type Food def eats(food: a.Food): Unit = {} def gets: a.Food } A cow is an animal that eats grass. A lion is an animal that eats meat. We can model these two animals by refining the trait Animal. trait Grass trait Meat trait Cow extends Animal with Meat { type Food = Grass def gets = new Grass {} } trait Lion extends Animal { type Food = Meat def gets = new Meat {} } Now, let's have leo the Lion eat milka the Cow. This is possible, because Cow is a subtype of Meat: val leo = new Lion {} val milka = new Cow {} leo.eats(milka) The lion leo can also eat whatever it gets: leo.eats(leo.gets) On the other hand, if we have lambda the (unknown) Animal, then we cannot feed it milka the Cow. After all, lambda the Animal might well be a Cow -or even, milka itself: val lambda: Animal = milka lambda.eats(milka) // type mismatch // found : Cow // required: lambda.Food
doi:10.1145/2714064.2660216 fatcat:5hs77tn3sfflljjdzh5htyn2eu