Self Types

A couple of remarks.

It is not just breaking an API as in making it unusable. It is introducing a potential TypeError at runtime, at a place where nobody expects it (within Kotlin code).

Example:

interface Foo {
   fun bar(): Self
}
 
fun test() {
  val x1: FooImpl1 = FooImpl1().bar()
  val x2: FooImpl2 = FooImpl2().bar()
}

This piece of code is completely written in Kotlin. Everything compiles just fine. No compile error whatsoever. However in line x2 a runtime TypeError is thrown. How can that be? The FooImpl2 was implemented in Java, and it did not return a FooImpl2. I omitted the implementation of FooImpl2 for the sake of simplicity.

I would note that Kotlin adds implicit non-null checks in every function that accepts non-null arguments. The only reason to do this is Java compatibility. It does not make the non-null types available in Java, but it deals with the potential problem with a fail fast behavior.

Something similar would need to be done for Self types. This could be a non-neglectible performance impact. The feature needs to be worth it. I’m not arguing that it is not worth it though. Just pointing out that it needs to be considered.

2 Likes

It is not just breaking an API as in making it unusable.

I think we’re saying the same thing. I understand that a Java dev could break things (and my opinion is that I’m personally ok with that, I know you disagree). My point was that there are cases, such as observers, where type-safety alone doesn’t solve the problem.

Another example is JS Events - an Event has a currentTarget property, which the observer would typically unsafely cast to the type of element that is being observed. This is ‘safe’ not because of the type system (or javascript’s lackthereof), but because that’s the contract. The observer can rely on currentTarget always being the object on which the handler was added.
In my specific case it’s Signals, but it’s the same idea. And in this specific case a Self type would eliminate the unsafe cast. It’s part of the contract, whether or not the language understands is irrelevant, it’s part of the contract and any sub-class that breaks that contract will have a runtime errors with or without Self type.

I’d like to clarify this discussion a little bit in terms of the work I’ve done in Manifold re self types. Specifically, I’ve observed an API employing Manifold self types implemented exclusively as a modifier on the return type do indeed work safely with Java projects not using Manifold. Without Manifold, from the Java perspective, the types are simply the declared base types – the Java compiler will complain if a return type is used as a more specific type, thus non-Manifold projects must cast the return type in such a case.

Therefore, self types as implemented in Manifold fall somewhere between #1 and #2.

What you describe is only one of 2 directions. It is clear that the described direction works safely. The problematic direction goes as follows:

manifold interface Foo {
    Self getBar();
}

java class FooImpl implements Foo {
    Foo getBar() {
        return new AnyOtherFooImpl()
    }
}

Now using FooImpl.getBar() from within manifold code is not safe, is it?

That’s an interesting example, but I would say that crosses over into the semantics of the implementation – there are many ways to break the semantic side of an interface contract. Further the example demonstrates how the Self type is an ideal way to statically prevent this type of mistake. In other words, if the manifold interface had not used the Self modifier, the Java code would still blow up at runtime, but perhaps in a less obvious way. Thus your example provides little weight as a counterexample, rather it serves as a use-case for using Self with Manifold.

Don’t get me wrong. I like the idea of self types. All I’m saying is it has its potential problems when mixing with Java code.

Sure. I’m just trying to provide a little clarity – I’m saying the self type as implemented in Manifold is not a danger to Java consumers not using Manifold. When you say “potential problems”, I am saying those problems remain in the absence of the self type, therefore they are inconsequential.

I don’t agree. As a consumer of a language that provides Self types I don’t expect to encounter this type of error. The API says it returns Self. For me it does not look like it can return anything other than Self. The Self type is supposed to move runtime error to static compilation errors. But when mixed with Java code, this goal is not achieved.

You are conflating the grammatical and semantic sides of an interface contract. For example, let’s say you don’t use the self type:

interface Builder {
  Builder withFoo( Foo foo );
}

The semantics of withFoo() require that it return the declaring instance. An implementation that does not return the proper type violates that contract. This is true regardless of whether Builder uses the self type to reinforce that contract.

With the self type, as a Java consumer not employing Manifold to enforce it, you can’t say you expect it to work either.

interface Builder {
  @Self Builder withFoo( Foo foo );
}

Think @NotNull. The same basic principles apply.

But I am not a Java consumer. I am a Manifold consumer. I did not write the misbehaving Java code. It might have come from a library.

Exactly. Kotlin has even better means to enforce non-nullability, and yet even they come with the same kind of problem when mixed with Java code. And that is why Kotlin is doing automatic non-null checks at runtime in order to fail fast, when the problem occurs.

Think about it this way. JetBrains uses their @NotNull annotation library in their APIs even though they cannot enforce its use with implementors of their APIs, such as plugins. You are essentially arguing that JetBrains should stop using that library.

My point with the Self type, both with Manifold and Kotlin, is to say it’s worthwhile even though some Java implementations may not use it and therefore may cause runtime errors. And I’ll point out again the runtime errors are a good thing because they catch the problem sooner; the same principle as the fail-fast null checks.

I have never argued against the worthiness of this feature. Other people can figure that out, and I am fine with that.

Update:

The Manifold framework now fully implements the Self type with @Self. Use @Self in the return type, parameter types, and field types. You can place it anywhere in a generic type as well:

public class Node<T> {
  private @Self Node<T> parent;
  private List<@Self Node<T>> children;

  public void add(@Self Node<T> child) {...}
  public List<@Self Node<T>> getChildren() {...}
}

The implementation is compatible with Java code not using the Manifold framework – no bridge methods or similar vulnerabilities involved. @Self-less Java code must, however, cast where necessary, which is a reasonable compromise. In my view, Manifold’s @Self demonstrates Kotlin can indeed provide a complete Self type and still maintain Java interop high standards.

I’ll also point out where @Self goes beyond Java’s recursive generics capabilities. Consider the equals() method:

public class MyClass {
  @Override
  public boolean equals(@Self Object obj) {
    ...
  }
}

This is a long time coming! Now the compiler can enforce MyClass symmetry with equals:

myClass.equals("notMyClass");  // compile error :)

While a self type sounds like it could be a great idea for Kotlin, I don’t think the equals() example helps.

For one thing, it’s possible for objects to equal subclass instances. (See here for a full explanation of the issues and how to fix them.) Restricting equals() as above would prevent that comparison in some cases.

(It’s even possible for two completely unrelated types to compare as equal, though it’s hard to think of a justification!)

And for another, I suspect it would further stretch Java compatibility.

For one thing, it’s possible for objects to equal subclass instances.

Yes, that’s true, but this is not an either/or proposition. If your class’s equals() implementation requires subclass equality, don’t use @Self for that class.

And for another, I suspect it would further stretch Java compatibility.

Perhaps. Any examples?

//kotlin
interface Foo {
    fun doSomething(): Self
}
// java
class Bar1: Foo {
    public Bar2 doSomemething() { return Bar2(); }
}
class Bar2: Foo {
    public Bar1 doSomething() { return Bar1();  }
}

This code would fail with a TypeCastException at runtime, since the java compiler does not realize that doSomething should return a self type and not just Foo.
Also I think this example was given here already.

Well, no, that example would not fail if implemented a la Manifold. Thus if Kotlin compiled Self in a similar way, it would look like this in bytecode:

public @Self Foo doSomething();

Therefore both Bar1 and Bar2 are valid; they both compile.

Also, if you are thinking about usage of the method failing from within a Kotlin call site:

(Bar2) bar2.doSomething(); // class cast exception

Yes, this fails, but this is a failure with the semantics of the method. In other words, if doSomething() is supposed to return the receiver type, that is a matter of Foo’s documentation and the failure of Bar2 to properly implement it – it would result in runtime failure with or without @Self.

Another benefit of using an annotation to implement the Self type is the builtin documentation it provides for other Java platform languages. For instance, an annotation processor could be used to enforce @Self when compiling Java against Kotlin APIs.

3 Likes

I found replacement of self feature, and I believe it cavers most of the cases when you would need self return. Extension functions are able to return instance of the extended class or generic type. It may look a bit not satisfying or very complex in case you need a lot of extension methods (e.g. for builder), but no more cyclic generic types and casts.

open class A() {
    fun setParent(x: Int) {
        //....
    }
}

class B() : A() {}

fun <T:A> T.setExtension(x: Int): T {
    setParent(x)
    return this
}

fun test() {
    val a = A().setExtension(1).setExtension(2).setExtension(3)
    val b = B().setExtension(2).setExtension(4)
    val ba: A = B().setExtension(2).setExtension(4)
}
1 Like

With this approach, you need write an additional extension method for every method. That’s a lot of boilerplate!

A similar solution is explained here: Emulating self types in Kotlin. DIY solution for missing language… | by Jerzy Chałupski | Medium. The main downside of that solution is that if the extension methods need to call methods or access properties, these methods and properties can’t be private, so it breaks encapsulation.

Those properties/methods can be internal or protected. Of course a naming convention and/or OptIn can also help. But the technique at least solves the problem from the consumer point of view although we don’t get the nice compile-time checking.

1 Like