Friday, 31 August 2007

Java 7 - Self-types

I just screamed at Java again. This time my ire has been raised by the lack of self-types in the language.

Self-types

So what is a self-type. Well, I could point you at some links, but I'll try and have a go at defining it myself...

A self-type is a special type that always refers to the current class. Its used mostly with generics, and if you've never needed it then you won't understand what a pain it is that it isn't in Java.

Here's my problem to help explain the issue. Consider two immutable classes - ZonedDate and LocalDate - both declaring effectively the same method:

 public final class ZonedDate {
   ZonedDate withYear(int year) { ... }
 }
 public final class LocalDate {
   LocalDate withYear(int year) { ... }
 }

The implementations only differ in the return type. This is essential for immutable classes, as you need the returned instance to be the same type as the original in order to call other methods (like withMonthOfYear).

Now consider that both methods actually contain the same code, and I want to abstract that out into a shared abstract superclass. This is where we get stuck:

 public abstract class BaseDate {
   BaseDate withYear(int year) { ... }
 }
 public final class ZonedDate extends BaseDate {
   // withYear removed as now in superclass, or is it?
 }
 public final class LocalDate extends BaseDate {
   // withYear removed as now in superclass, or is it?
 }

The problem is that we've changed the effect of the method as far as ZonedDate and LocalDate are concerned, because they no longer return the correct type, but now return the type of the superclass.

One solution is to override the abstract implementation in the subclass, just to get the correct return type (via covariance):

 public abstract class BaseDate {
   BaseDate withYear(int year) { ... }
 }
 public final class ZonedDate extends BaseDate {
   ZonedDate withYear(int year) {
     return (ZonedDate) super.withYear(year);
   }
 }
 public final class LocalDate extends BaseDate {
   LocalDate withYear(int year) {
     return (LocalDate) super.withYear(year);
   }
 }

What a mess. Imagine doing that for many methods on many subclasses. Its a large amount of pointless boilerplate code for no good reason. What we really want is self-types, with a syntax such as <this>:

 public abstract class BaseDate {
   <this> withYear(int year);
 }
 public final class ZonedDate extends BaseDate {
   // withYear removed as now correctly in superclass
 }
 public final class LocalDate extends BaseDate {
   // withYear removed as now correctly in superclass
 }

The simple device of the self-type means that the withYear method will now appear to have the correct return type in each of the subclasses - LocalDate in LocalDate, ZonedDate in ZonedDate and so on. And without reams of dubious boilerplate.

Summary

Self-types are a necessary companion for abstract classes where the subclass is to be immutable (and there are probably other good uses too...).

Opinions welcome as always!

22 comments:

  1. Herman van Hovell31 August 2007 at 16:03

    This is especially annoying with creating more fluent interfaces...

    With Java 5.0 with generics there is a work around though. Declare a self referencing parameter on the abstract class. Such as:

    public abstract class BaseDate>
    {
    E withYear(int year)
    {
    ...;
    // This will raise an unchecked warning...
    return (E)this;
    }
    }

    public final class ZonedDate extends BaseDate
    {
    // Same effect as self type...
    }

    public final class LocalDate extends BaseDate
    {
    // Same effect as self type...
    }

    Although I do admit that this is not the prettiest syntax (..it also lends itself to abuse), its a bit better than tons of covariant overrides.

    Hope this helps.

    Cheers,

    Herman van Hovell

    ReplyDelete
  2. Why wouldn't the following piece of code work?

    public abstract class BaseDate {
    T withYear(int year) { return (T) this; }
    }

    public final class ZonedDate extends BaseDate {
    ZonedDate withYear(int year) {
    return super.withYear(year);
    }
    }

    public final class LocalDate extends BaseDate {
    LocalDate withYear(int year) {
    return super.withYear(year);
    }
    }


    Am I missing something?

    ReplyDelete
  3. Stephen Colebourne31 August 2007 at 16:58

    You're correct, that would work too. But, now you've exposed an implementation detail to all users of the API. Specifically, you can't hold a reference to a BaseDate without specifying the implementing subclass, which is a little daft.

    ReplyDelete
  4. The biggest problem with John's solution is that if BaseDate is concrete, then you need to refer to it as BaseDate everywhere or face a raw-type warning.

    ReplyDelete
  5. I'd like to see an implementation of the first solution (the one with typecasts in) that won't throw a ClassCastException.

    ReplyDelete
  6. @Ricky: getClass().newInstance() ?

    @Stephen: I'm desperate for self-types. Current Generics force exposing implementation details in cases like mentioned, which is a pain in the butt. This would also solve the Object.clone() problem. Could also help with implementing Comparable.
    It's quite a problem to build a class thinking ahead of what subtypes may need beforehand.

    ReplyDelete
  7. Stephen Colebourne31 August 2007 at 18:11

    @Ricky, you have a single abstract factory method in the superclass implemented by each subclass, or use getClass().newInstance() ;-)

    ReplyDelete
  8. I think self types are a good idea, though I wonder if they are among the low hanging fruit for improving the language. The keyword "this" could be used to mean the current type because of Java's separation of namespaces. Few people have written about what the spec would look like; self types would have to be restricted to covariant contexts (i.e. not allowed as an argument type).

    ReplyDelete
  9. I submitted a comment, however I kept getting the message "Your comment was marked as spam and will not be displayed." What gives?

    ReplyDelete
  10. This seems pretty questionable to me. It doesn't make much sense to have a base class that refers to the actual class of its children. OO would suggest that if a subclass wants to constrain the interface of its base class it should override the behavior. If you have a situation where you're doing this a lot then you probably aren't dealing with a subclass at all.

    ReplyDelete
  11. getClass().newInstance() is clearly flawed in the common case that there is no public no-arg constructor (it places a contract on subclasses that is not expressible in the type system). The abstract factory is the one way that works.

    I haven't used subclassing in Java for a few years now, initially as an experiment to see whether I could do without it, and now because I can't see any cases where I'd want it. Does the self-typing issue manifest itself without subclassing?

    ReplyDelete
  12. Stephen Colebourne31 August 2007 at 19:46

    @Neal, I deliberately used the angle brackets around this to mark out the self-type as related to generics. Personally, I'd like to see a self-type usable in method parameters too, but I've heard before that there is some deep and dark reason why it doesn't work :-)

    @Matthew, I didn't mark your comment as spam, JRoller did. I've no idea why, but I've found it now and let it through.

    @Ocean, The superclass isn't referring directly to the class of the subclass, its simply telling the compiler to auto-implement covariant types. I don't believe that it has anything to do with OO. I have noticed that for me, this is a problem primarily around designing APIs with immutable classes. Immutables are still pretty rare in Java, but with the forthcoming multi-processor world, immutables will become more common.

    @Ricky, I believe self-types are only useful wrt subclasses.

    ReplyDelete
  13. > Personally, I'd like to see a self-type usable in method
    > parameters too, but I've heard before that there is some deep
    > and dark reason why it doesn't work :-)

    Isn't it basically the same reason why there aren't "covariant parameter types"? If BaseDate could have a foo() method, and you just have a reference to a BaseDate, what value could you safely pass to it, not knowing the receiver's actual class? It would seem that foo could only be invoked if the type of the target expression were a final class.

    ReplyDelete
  14. Stephen Colebourne1 September 2007 at 10:02

    @Anonymous, Yes, that sounds correct:

    BaseDate date = ...
    date.foo(new ZonedDate());

    We can't tell if this is valid, because we don't know that date is z ZonedDate instance. One solution would be to throw a ClassCastException at runtime if it was invalid, which isn't ideal.

    ReplyDelete
  15. If you want to use runtime features at compile time, than don't try to extend Java with a new cryptical syntax. Change the language of implementation, resp. the language needs to change.

    Nowadays it should be the job of the IDE to validate the structure of the code at coding time, rather than the compiler. The problem above wouldn't bother you, if you (and the user's of your API, of course) got the correct IntelliSense when needed.

    IMHO ...

    ReplyDelete
  16. I can't post code on this site - it gets marked as spam! Can you fix that?

    ReplyDelete
  17. Self types have some uses but they can also be a problem. When they were introduced in Eiffel v3 the example given was a ski camp were you don't wan't the boys to share rooms with the girls. The classes were something like:

    abstract class Skier {
    this roomMate;
    this getRoomMate() { return roomMate; }
    void setRoomMate( this roomMate ) { this.roomMate = roomMate; }
    }

    class Boy extends Skier {}

    class Girl extends Skier {}

    The disadvantage is that when the example was extended to include ranked skiers then a ranked boy couldn't share with a none ranked boy. If self referencing generic types are used instead then this isn't a problem, e.g.:

    abstract class Skier< S extends Skier< S > > {
    S roomMate;
    S getRoomMate() { return roomMate; }
    void setRoomMate( S roomMate ) { this.roomMate = roomMate; }
    }

    class Boy extends Skier< Boy > {}

    class Girl extends Skier< Girl > {}

    class RankedBoyextends Skier< Boy > {}

    class RankedGirl extends Skier< Girl > {}

    And a ranked skier can share with a none ranked skier. Therefore self types introduce other problems. Is there a way of getting the best of the two systems?

    ReplyDelete
  18. > We can't tell if this is valid, because we don't
    > know that date is z ZonedDate instance. One
    > solution would be to throw a ClassCastException at
    > runtime if it was invalid, which isn't ideal.

    On method parameters, only contra-variance would be possible, not sure if that's of any use in current Java. Otherwise, one would need runtime binding of methods, I think.

    ReplyDelete
  19. Hi stephen, peter ahé wrote a blog about self type
    http://blogs.sun.com/ahe/entry/selftypes
    last year.

    In you case, you can hide implementation details
    by using a non-public abstract class between
    BaseDate and its subclasses.

    Rémi

    ReplyDelete
  20. I think what is missing are mixins. You would still need the self-type facility, but a mixin models this problem better than a base class, IMHO.

    ReplyDelete
  21. Stephen Colebourne4 September 2007 at 14:42

    @Stefan, I suspect I was thinking of erased method parameters, so only the subclasses would be real method overrides.

    @Remi, yes you are probably correct that a hidden class between BaseDate and its subclasses would work. However, it won't be acceptable if BaseDate is a class that is intended to be subclassed and added to the JDK!

    ReplyDelete
  22. This can be achieved using recursive generic types. See below. Note that I have used square brackets in place of angle brackets, as they did not render correctly.


    public class DateTest
    {
    @Test
    public void testDate()
    {
    ZonedDate withYear = new ZonedDate().withYear(0);
    }
    }

    abstract class BaseDate[T extends BaseDate[T]]
    {
    public T withYear(final int year)
    {
    return null;
    }
    }

    final class ZonedDate extends BaseDate[ZonedDate]
    {
    // some other logic
    }

    final class LocalDate extends BaseDate[LocalDate]
    {
    // some other logic
    }

    ReplyDelete