Skip to content

Latest commit

 

History

History
183 lines (138 loc) · 5.84 KB

2. Existng solutions review.md

File metadata and controls

183 lines (138 loc) · 5.84 KB

Existing self-type implementations review

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

Academy

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:

To be able to safely type some non-receiver values with self-type, scientific papers propose to introduce special kinds of methods:

TypeScript

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).

Python

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 Manifold

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");

Swift

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(/* ... */)
}