Surprisingly, there is a lot of research around self-types in the academy and not so many implementations in industrial languages.
Things that we are interested at most are:
- Values of self-type: only receiver (
this
) has self-type or something else - Permitted positions of self-types
Self-type is in fact a type parameter of a recursive type bounded with mu-notation. It can be safely used in covariant positions, but if self-type occurs in the contravariant position, then inheritors are not subtypes anymore ([Inheritance is not subtyping, Cook at al., 1990]). So a lot of academic efforts are directed to solve this misconception and to use safely self-types in contravariant positions preserving useful applications:
- Use matching relation instead of subtyping ([Bruce et al., 1996])
- Explicitly distinguish exact and existential types ([Saito et al, 2009])
- Named wildcards and exact type parameters ([Na et al, 2012])
To be able to safely type some non-receiver values with self-type, scientific papers propose to introduce special kinds of methods:
- Nonheritable methods: ([Saito et al, 2009])
- Virtual constructors ([Na et al, 2012])
https://www.typescriptlang.org/docs/handbook/2/classes.html#this-types
Implementation of self-types is not safe due to lack of restrictions on the contravariant positions:
class Box {
content: string = "";
sameAs(other: this): boolean {
return other.content === this.content;
}
}
class DerivedBox extends Box {
otherContent: string = "?";
sameAs(other: this): boolean {
if (other.otherContent === undefined) {
console.log("TS type system is broken")
}
return other.otherContent === this.otherContent;
}
}
const base = new Box();
const derived = new DerivedBox();
function test(x: Box): boolean {
return x.sameAs(base)
}
test(derived) // prints: TS type system is broken
The only inhabitant of this-type is the receiver (this
).
https://peps.python.org/pep-0673/
Self-type implementation strategy in Python is to convert them back to the TypeVar
with recursive constraint.
The design of self-type in Python is similar to the TypeScript with no safety guarantees and receiver as the only inhabitant.
Java compiler plugin Manifold allows annotating a type with @Self
:
https://github.com/manifold-systems/manifold/tree/master/manifold-deps-parent/manifold-ext\#the-self-type-with-self
class VehicleBuilder {
/* ... */
public @Self VehicleBuilder withWheels(int wheels) {
_wheels = wheels;
return this;
}
}
class AirplaneBuilder extends VehicleBuilder {
/* ... */
}
Airplane airplane = new AirplaneBuilder()
.withWheels(2) // : AirplaneBuilder
.withWings(1);
It is also possible to use @Self
in the input position.
It provides some additional type restrictions:
class A {
public boolean equals(@Self Object obj) {
/* ... */
}
}
A a = new A();
a.equals("String instead of A"); // compilation error
However, such restrictions can be easily got around.
So canonical isinstance
check in the equals
method is still needed:
Object obj = a;
obj.equals(String instead of A); // no compilation error
Other covariant positions of @Self
are also allowed and contravariant ones still require runtime checks:
public class Node {
private List<Node> children;
public List<@Self Node> getChildren() {
return children;
}
public void addChild(@Self Node child) {
checkAssignable(this, child); // Необходима проверка
children.add(child);
}
}
public class MyNode extends Node {
/* ... */
}
MyNode myNode = findMyNode();
List<MyNode> = myNode.getChildren();
Manifold allows declaring extension functions in Java where @Self
is an extension receiver type:
public static <K,V> @Self Map<K,V> add(
@This Map<K,V> thiz, K key, V value) {
thiz.put(key, value);
return thiz;
}
HashMap<String, String> map = new HashMap<>()
.add("nick", "grouper")
.add("miles", "amberjack");
Self-type in classes can only be used in return positions, and the only inhabitant is a receiver (self
).
Class that conforms a protocol with self-types in other positions should replace them by itself:
protocol P {
func produce() -> Self
func consume(_ x: Self)
}
class C: P {
func produce() -> Self { return self }
func consume(_ x: #\framebox{C}#) {}
}
When a protocol is used as type parameter bound, this type parameter replaces self-types on method invocation:
func testConstrait<T: P>(_ x: T) {
x.consume(/* value of type T is expected */)
}
Protocols can be bounds for existential types. In that case, method call with self-type non-return positions is prohibited:
func testAny(_ x: any P) {
// error: member 'consume' cannot be used on value of type
// 'any P'; consider using a generic constraint instead
x.consume(/* ... */)
}