Scala: Abstract Types

The basic idea of abstract typing in Scala is leaving undefined some types when implementing a new abstract class. This types will be defined on subclasses. Additionally, you can specify some restrictions about the hierarchy of this unknown type as: Must be subclass of or Must be superclass of. This is called lower and upper bounds.
To show the potential of this tools, the same example will be developed with four different approaches. The following objects must be modelled:

  • Generic trash.
  • Different kinds of trash, must inherit from the abstract one.
  • An abstract recycling container. Must contain common logic.
  • Different kinds of recycling containers.
  • At least one recycling container must inherit from a non-abstract one.

Simple and buggy solution

Here there is the implementation of the trash and its hierarchy. There is one generic trash called Trash, and two specializations Paper and Glass. Finally, there’s a subtype of Glass, which is WhiteGlass.

public class Trash{} public class Paper extends Trash{} public class Glass extends Trash{} public class WhiteGlass extends Glass{}

This is the simplest way to implement a generic recycling container. It should contain some logic which is not relevant for this example. The main method takes an object of type Trash. This decision will be revealed as simply wrong when implementing the subclasses.

public abstract class RecyclingContainer{ public abstract void fill(Trash trash); //some common logic. }

When creating the subtype for paper container, we realize that the previous abstract class was a bad choice. We can’t override the method taking Paper instead of Trash.

public class PaperContainer extends RecyclingContainer{ @Override public void fill(Paper paper){} }

The only thing that can make this class compile is creating a method fill that takes Trash.

public class RecyclingContainer{ public void fill(Trash trash){} }

Obviously, this solution is very buggy. In only two lines we can do something forbidden.

PaperContainer cont = new PaperContainer(); cont.fill(new Glass()); //glass in a paper container!!!

Java Generics Solution I

As using a simple solution is not working, the complexity of the code must be increased. We will create a class using generics. We want an abstract recycling container being able to carry all types of trash. This class will use Generics and the method fill will take an object of type E (any class inheriting from Trash).

public abstract class RecyclingContainer<E extends Trash>{ public abstract void fill(E trash); //some common logic. }

PaperContainer and GlassContainer will extend this abstract class delimiting the genetic type E to the proper kind of trash.

public class PaperContainer extends RecyclingContainer<Paper>{ public void fill(Paper paper){} } public class GlassContainer extends RecyclingContainer<Glass>{ public void fill(Glass glass){} }

However, this solution could not accomplish the 5th goal.

public class WhiteGlassContainer extends GlassContainer{ public void fill(WhiteGlass whiteGlass){} //won't compile }

Java Generics Solution II

Increasing again the complexity of this solution we reach the most robust option.
We will keep RecyclingContainer as it is but we will modify its subclasses. This new subclasses will lay also on the use of generics. Every fill method will take an object of the undefined type E.

public class PaperContainer<E extends Paper> extends RecyclingContainer<E>{ public void fill(E paper){} } public class GlassContainer<E extends Glass> extends RecyclingContainer<E>{ public void fill(E glass){} } public class WhiteGlassContainer<E extends Glass>; extends GlassContainer<E>{ public void fill(E whiteGlass){} }

Using this classes in a correct way, it’s easy to recycle properly:

//with a glass container RecyclingContainer<Glass> container = new GlassContainer<Glass>(); //you can add glass container.fill(new Glass()); //and white glass container.fill(new WhiteGlass()); //but no paper container.fill(new Paper()); //won't compile //and you can't "dress" a glass container like a generic one... RecyclingContainer<Trash> nonComp = new GlassContainer<Glass>(); //won't compile

However, if the developer of the client side doesn’t use generics properly, some forbidden actions can happen in runtime.

//he haven't heard about Generics! RecyclingContainer buggy = new PaperContainer(); buggy.fill(new Paper()); //and he managed to ruin the recycling plant buggy.fill(new Glass());//compiles, but throws a ClassCastException

Scala Approach

Scala provides a more robust solution to this problem. The abstract container will be created with an unknown type, called SuitableTrash in this case.

abstract class RecyclingContainer{ type SuitableTrash <: Trash def fill(trash : SuitableTrash) } class PaperContainer extends RecyclingContainer{ type SuitableTrash = Paper def fill(trash : Paper) {} } class GlassContainer extends RecyclingContainer{ type SuitableTrash = Glass def fill(trash : Glass) {} } class WhiteGlassContainer extends RecyclingContainer{ type SuitableTrash = WhiteGlass def fill(trash : WhiteGlass) {} }

This solution is safer and even inexperienced developers can’t break the logic of these classes.
* As Volker noticed, there was a problem in the code pasted, WhiteGlassContainer must extend RecyclingContainer. This means that we can’t accomplish last point of the requirements. Suggestions are welcome.

//You can fill the glass container with all types of glass val container = new GlassContainer container fill (new Glass) container fill (new WhiteGlass) //but no with paper container fill (new Paper) //won't compile //Trying to John Buggy's issue val buggy : RecyclingContainer = new GlassContainer buggy fill (new Paper) //won't compile //however neither this won't compile buggy fill (new Glass) //won't compile

In conclusion, Java provides powerful tools for checking types and avoid runtime exceptions. However, they are only applied if the developer coding the client class is programming using them. Additionally, Java is verbose enough to make things a little more complicated. Scala instead, offers a simpler and cleaner solution for the same problem which is safer than the Java one.

7 Responses

  1. Crec que ja es hora que modifiquis el “about” ;)

  2. Dear Josep,

    thank you for sharing – good Article.

    You can extend your solution a little: As long as Paper, Glass and WhiteGlass are doing similar things in the fill method (e.g. fill a Container), you can improve your solution:

    abstract class RecyclingContainer{
    type SuitableTrash <: Trash

    def fill(trash : SuitableTrash) {
    // do implementation here
    }
    }

    Now you can use the following class

    class MyPaperContainer extends RecyclingContainer{
    type SuitableTrash = Paper
    // It will use the fill method of the abstract class
    // – but it will only accept Paper …
    }

    Best Regards,
    Marcus

    • Dear Marcus,
      thank you for leaving a comment.
      It’s true, your solution is cleaner and I would use it in a real world problem. However, as it was only an academic exercise, I wanted to override the methods to make it clearer.

      Best,
      Josep

  3. Solution II (using generic ||) is not good

    • Hi ratn,
      Thank you!
      There was a mistake with copy paste all content between < and > was removed from the examples.

      However, if you have found a more robust solution, feel free to share it, please.

  4. Dear Josep,

    the scala example doesn’t work for me, the line “type SuitableTrash = WhiteGlass” leads to an error. Dean Wampler wrote in “Programming Scala”: “it is not possible to override a concrete type definition”. Do you know a good workaground?

    Best Regards,
    Volker

    • Dear Volker,
      thank you for reporting this mistake, I added one note for this correction in the post. As we are defining the abstract type in WhiteGlassContainer, this must extend RecyclingContainer (where abastract type isn’t defined yet).
      The problem came when overriding the abstract type in a class that extends a class where this type was already defined. I will look for a different approach in order to accomplish all requirements.

Leave a reply to Marcus Bosten Cancel reply