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


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.


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.