Selfishness
Most statically typed object-oriented languages allow a group of related methods to be specified together as an interface (or "trait", "protocol", etc.). Self types are a feature that allows the types in such an interface to refer to the type implementing that interface.
Using self types as method arguments allows precise typechecking of
binary methods1 such as equals
, where x.equals(y)
requires a y
of the same type as x
. The lack of such types in
e.g. Java means that the Object.equals
method must usually include a
cast that can fail at runtime, as its type allows any other object to
be passed.
Using self types as returns allows precise typechecking of methods
that return this
, or methods that copy the receiving object.
However, the presence of self types breaks some properties of the type system, and unsoundness arises if other parts of the system rely on them.
-
Argument self types break the property that, if a class
C
has a subclassD
and both implement an interfaceP
, then aD
can be used anywhere aC
is expected. The issue is thatP
may include a method that accepts a self type, and this method inD
works on a narrower class of inputs than the corresponding method inC
. (See Subtyping vs. inheritance)class c (name : string) = object (self : 'self) method name = name method equals (x : 'self) = (name = x#name) end class d (name : string) (size : int) = object inherit c name as super method size = size method equals (x : 'self) = (super#equals x && size = x#size) end let sub (x : d) = (x :> c) (* OCaml correctly rejects this coercion: despite inheriting from it, d is not a subtype of c *)
-
Returned self types break the property that, if a class
C
implements an interfaceP
, and subclassD
inherits all of its behaviour fromC
, thenD
also implementsP
.The issue is that
P
may include a method that returnsSelf
, whichC
implements by returning a newC
. When this method is inherited byD
it still returnsC
, even thoughP
now requires that it returnD
. This led to a soundness issue in Swift2, and a related issue in Rust3:// Counterexample by Hamish Knight protocol P { associatedtype X where X == Self func foo() -> X } class C : P { typealias X = C func foo() -> X { return C() } } class D : C { var name = "D" } func foo<T : P>(_ x: inout T) { x = x.foo() } var d = D() foo(&d) print(d.name) // crashes
// Counterexample by Niko Matsakis trait Make { fn make() -> Self; } impl Make for *const uint { fn make() -> *const uint { ptr::null() } } fn maker<M:Make>() -> M { Make::make() } fn main() { let a: *uint = maker::<*uint>(); // we have "produced" a *uint even though there is no // function in this program that returns one. }
An alternative to self types is to use generic interfaces: instead of
an interface with an equals
method accepting a self
type, one can use a generic interface whose equals
method accepts an , and then write classes that implement
. This is the approach taken by C#'s
IEquatable<T>
and Java's Comparable<T>
.
On Binary Methods, Kim Bruce, Luca Cardelli, Giuseppe Castagna, The Hopkins Objects Group, Gary T. Leavens, and Benjamin Pierce (1995)