Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala: abstract classes with anonymous types

I am reading "Scala for the Impatient" and in 8.8 they say:

[..] you can use the abstract keyword to denote a class that cannot be instantiated [..]

abstract class Person { val id: Int ; var name: String }

And several lines later:

You can always customize an abstract field by using an anonymous type:

val fred = new Person {

  val id = 1729

  var name = "Fred"

}

So, they artificially instantiated Person class with the anonymous type. In which situations in the real world one would want to do it?

like image 418
Alina Avatar asked Apr 01 '18 13:04

Alina


1 Answers

After thinking about my own answer a little bit, I concluded that all it says is essentially just:

"Anonymous local class instances are poor man's function literals"

Offered a +150 bounty for an answer that helps expand this narrow vision.


TL;DR

Whenever you want to treat implementations of methods as objects, you can instantiate an anonymous local class that extends an abstract base class, implement the methods, and then pass the created instance around just like any other instance of the base class.


Overview

This posting discusses five situations in which you might want to instantiate anonymous local classes. The examples progress from very basic to fairly advanced.

  1. Simple example with Runnable
  2. Simple example with plotting 2d-functions
  3. A historically important example Function<X, Y>
  4. An advanced real-world example where the instantiation of anonymous local classes seems unavoidable
  5. Brief discussion of the code that you used to introduce your question.

Disclaimer: some of the code is non-idiomatic, because it "reinvents the wheel" and does not hide the instantiation of abstract local classes in lambdas or SingleAbstractMethod-syntax.


Simple introductory example: Runnable

Suppose that you want to write a method that takes some block of code, and executes it multiple times:

def repeat(numTimes: Int, whatToDo: <someCleverType>): Unit = ???

Assuming that you want to reinvent everything from scratch, and do not want to use any by-name parameters or interfaces from standard libraries, what do you put in place of <someCleverType>? You would have to provide your base class that looks somewhat like this:

abstract class MyRunnable {
  def run(): Unit  // abstract method
}

Now you can implement your repeat method as follows:

def repeat(numTimes: Int, r: MyRunnable): Unit = {
  for (i <- 1 to numTimes) {
    r.run()
  }
}

Now suppose that you want to use this method to print "Hello, world!" ten times. How do you create the right MyRunnable? You could define a class HelloWorld that extends MyRunnable and implements the run method, but it would only pollute the namespace, because you want to use it only once. Instead, you can instantiate an anonymous class directly:

val helloWorld = new MyRunnable {
  def run(): Unit = println("Hello, world!")
}

and then pass it to repeat:

repeat(10, helloWorld)

You could even omit the helloWorld variable:

repeat(10, new MyRunnable {
  def run(): Unit = println("Hello, world!")
})

This is a canonical example of why you would want to instantiate anonymous local classes.


Slightly more interesting example: RealFunction

In the previous example, the run took no arguments, it executed the same code every time.

Now I want to modify the example slightly, so that the implemented method takes some parameters.

I will not provide full implementations now, but suppose that you have a function

plot(f: RealFunction): Unit = ???

that plots a graph of a real function R -> R, where RealFunction is an abstract class defined as

abstract class RealFunction {
  def apply(x: Double): Double
}

To plot a parabola, you could now do the following:

val xSquare = new RealFunction {
  def apply(x: Double): Double = x * x
}

plot(xSquare)

You could even test it separately without plot: for example, p(42) computes 1764.0, which is the square of 42.


General functions Function[X, Y]

The previous example generalizes to arbitrary functions, which can have types X and Y as domain and codomain. This is arguably the most important example from the historical point of view. Consider the following abstract class:

abstract class Function[X, Y] {
  def apply(x: X): Y // abstract method
}

It is similar to the RealFunction, but instead of fixed Double, you now have X and Y.

Given this interface, you could re-create the xSquare function as follows:

val xSquare = new Function[Double, Double] {
  def apply(x: Double) = x * x
}

Indeed, this example is so important that Scala's standard library is filled with such interfaces FunctionN[X1,...,XN, Y] for varying numbers of arguments N.

These interfaces get their own concise syntax and are otherwise heavily privileged in the compiler. This creates a "problem" from the point of view of your question, because the instantiation of anonymous classes is usually hidden under special built-in syntactic sugar. In idiomatic Scala, you would usually simply write

val xSquare = (x: Double) => x * x

instead of

val xSquare = new Function[Double, Double] {
  def apply(x: Double) = x * x
}

The situation is similar in other JVM languages. For example, even Java version 8 introduced bunch of very similar interfaces in java.util.function. Few years ago, you would have written something like

Function<Integer, Integer> f = new Function<Integer, Integer>() {
  public Integer apply(Integer x) {
    return x * x;
  }
};

in Java, because there were no lambdas yet, and every time you wanted to pass some kind of callback or Runnable or Function, you had to implement an anonymous class that extends an abstract class. Nowadays, in newer Java versions it is hidden by the lambdas and the SingleAbstractMethod-syntax, but the principle is still the same: the construction of instances of anonymous classes implementing an interface or extending an abstract class.


An advanced "almost-real-world"-example

You will not encounter any of the previous examples in the code written today, because the instantiation of anonymous local classes is hidden by syntactic sugar for lambdas. I want to provide a realistic example where the instantiation of anonymous local classes is actually unavoidable.

The new AbstractClassName(){ }-syntax still appears where no syntactic sugar is available. For example, because Scala has no syntax for polymorphic lambdas, to construct a natural transformation in a library like Scalaz or Cats, you would usually write something like:

val nat = new (Foo ~> Bar) {
  def apply[X](x: Foo[X]): Bar[X] = ???
}

Here, Foo and Bar would be something like embedded domain specific languages that operate on different levels of abstraction, and Foo is more high-level, whereas Bar is more low-level. It's exactly the same principle again, and such examples are everywhere. Here is an almost "photo-realistic" example of real-world usage: defining an (KVStoreA ~> Id)-interpreter. I hope that you can recognize the new (KVStoreA ~> Id) { def apply(...) ... } part in there. Unfortunately, the example is fairly advanced, but as I mentioned in the comments, all the simple and frequently used examples have been mostly hidden by lambdas and Single-Abstract-Method syntax over the past decade.


Back to your example

The code that you quoted

abstract class Person(val name: String) {
  def id: Int
}

val fred = new Person {
  val id = 1729
  var name = "Fred"
}

does not seem to compile, because the constructor argument is missing.

My guess is that the author wanted to demonstrate that you can override defs by vals:

trait P {
  def name: String
}

val inst = new P {
  val name = "Fred"
}

While it's good to know that this is possible, I don't consider this the most important use case for anonymous local class instantiation (because you could have used an ordinary member variable and pass the value in the constructor instead). Given the space constraints, the author of the book probably just wanted to quickly demonstrate the syntax, without going into extended discussions of the real-world usage of that.

like image 56
Andrey Tyukin Avatar answered Sep 16 '22 15:09

Andrey Tyukin