How do anonymous subclasses work

I have a case class named EarthMap which represents a mapping of the earth (in terms of longitude and latitude) onto a rectangular image. By default there is a one to one correspondence between pixels in the image and degrees on the Earths surface for example. I.e. an area of the earth corresponding to dx=1 degree longitude and dy=1 degree latitude corresponds to one pixel in the image.

earth-flat

The class also supports zoom windows (for clipping and scaling).

I’d like to extend the class to support cartography projections. For example Mercator projection and Peters projection.

earth-peters

This means that the class needs to know about a cartography projection function and inverse function. The question is from an API perspective, how should I specify this projection?

I have noticed that with EarthMap being a case class, I can instantiate an instance using the syntax var em = EarthMap(). However, the syntax var em = EarthMap(){var var1=100} is marked as an error but var em = new EarthMap(){var var1=100} is not marked as an error.

What is the logic and intuition of when I am allowed to instantiate anonymous subclasses such as new EarthMap(){...} and which kinds of extensions and overrides I’m allowed to denote? Also in the past, I’ve shied away from subclasses of case classes, simply because I don’t understand the consequences such as unexpected changes in equality semantics.

For example, if EarthMap has methods project and invProject, am I supposed to be able to override those methods in an anonymous subclass as follows? (BTW to my surprise it seems to work.)

case class EarthMap(...) {
  def project(loc:Location) = (loc.lat -> loc.lon)
  def invProject(lat:Double,lon:Double) = Location(lat,lon)
 ...
  }

  def main(argv: Array[String]): Unit = {
    import math._
    val em = new EarthMap() {
      override def project(loc: Location):(Double,Double) = (90 * sin(toRadians(loc.lat)) -> loc.lon)

      override def invProject(lat: Double, lon: Double):Location = Location(toDegrees(asin(lat / 90)), lon)
    }
  ...
}

This seems on my naive attempt to work fine, but I’m skeptical. I don’t understand the difference between new EarthMap and EarthMap(). Isn’t the former skipping any extra initialization in the apply method of the object EarthMap declaration?


val e1 = EarthMap()  //  Call to EarthMap.apply()

val e2 = new EarthMap()  //  Call to no-argument constructor

val e3 = EarthMap() { ... }  // Call to EarthMap.apply()(...), the { ... } block is second argument

val e4 = new EarthMap() { ... }  //  Creates instance of anonymous class, the { ... } block is class definition

If you sub-class a case class, the case class equality remains intact as long as you don’t override equals or hashCode. But that would mean that objects of different classes might now be equal, e.g.


case class A(i: Int, s: String)

class B(i: Int, s: String, val l: Long) extends A(i, s)

val a1 = new A(1, "yo")

val b1 = new B(1, "yo", 10L)

// now, a1 == b1, but does that make sense?

1 Like

The new EarthMap() version is normal Scala syntax – that’s the way classes ordinarily work.

The EarthMap() syntax is special magic that only works for case classes (in Scala 2; this may change in Scala 3), and when you subclass it, it’s not a case class any more. (Indeed, it’s strictly illegal to subclass a case class with another case class.) When you say

var em = new EarthMap(){var var1=100}

you’re defining an anonymous subclass (not a case class) of EarthMap, so you need to use the new syntax.

That works, although it’s worth noting that anonymous subclasses are unusual. More commonly, you would say something like:

class MercatorMap(...) extends EarthMap(...) {
  ...
}
val em = new MercatorMap(...)

That’s a matter of taste and circumstance, though – the anonymous-subclass approach is legal, if somewhat uncommon.

And yes, overrriding def’s in a subclass, the way you are doing, is entirely normal – that’s more or less orthodox OO-style programming. (Less common in pure FP code, but that’s not the style you’re using.) I would usually recommend against adding additional vals in a subclass of a case class (which tends to lead to equality misery), but adding or overriding methods is mostly fine…

1 Like

I took the advice suggested above and declared a PetersEarthMap class. But I find later that I can’t initialize an instance thereof as flexibly as I thought I might.

class PetersEarthMap extends EarthMap {
  // peters projection
  override def project(loc: Location):(Double,Double) = (90 * sin(toRadians(loc.lat)) -> loc.lon)
  override def invProject(lat: Double, lon: Double):Location = Location(toDegrees(asin(lat / 90)), lon)
}

case class EarthMap(locationLL:Location=Location(-90,-180), // location on the globe in latitude;longitude
                    locationUR:Location=Location(90,180), // location on the globe in latitude;longitude
                    width:Int=360, height:Int=180, // size in pixels of the image file
                    borders:Boolean=true,
                    colors:Iterable[(Double,Color)] = Array(
                      // celsius -> color
                      (60.0, Color(255, 255, 255)),
                      (32.0, Color(255, 0, 0)),
                      (12.0, Color(255, 255, 0)),
                      (0.0, Color(0, 255, 255)),
                      (-15.0, Color(0, 0, 255)),
                      (-27.0, Color(255, 0, 255)),
                      (-50.0, Color(33, 0, 107)),
                      (-60.0, Color(0, 0, 0)))
                   ) {
  var palette:ColorPalette = ColorPalette(colors)
  val Location(latMin: Double, lonMin: Double) = locationLL
  val Location(latMax: Double, lonMax: Double) = locationUR
...
}

But then later when I tried to create an instance of PetersEarthMap I get a compiler error

Error:(256, 43) unknown parameter name: colors
        val em = new PetersEarthMap(colors=Array(
Error:(256, 18) no arguments allowed for nullary constructor PetersEarthMap: ()globe.PetersEarthMap
        val em = new PetersEarthMap(colors=Array(

Can I solve this with a simply sytanctical difference in the class declaration, or is this really something the object system does not support?

When I declare a subclass, do I lose the initialization options of the superclass?

val em = new PetersEarthMap(colors=Array(
          (60.0, Color(255, 255, 255)),
          (0.0, Color(0, 255, 255)),
          (-60.0, Color(0, 0, 0)))

I also discovered that I can define PetersEarthMap as follows

class PetersEarthMap(colors:Iterable[(Double,Color)]) extends EarthMap(colors=colors) {
  // peters projection
  override def project(loc: Location):(Double,Double) = (90 * sin(toRadians(loc.lat)) -> loc.lon)
  override def invProject(lat: Double, lon: Double):Location = Location(toDegrees(asin(lat / 90)), lon)
}

in which case I can instantiate it as follows

        val em = new PetersEarthMap(colors=Array(
          // celsius -> color
          (10.0, Color(255, 0, 0)),
          (0.0, Color(0, 255, 0)),
          (-10.0, Color(0, 0, 255))))

However, in this case I can no longer instantiate it using the default colors of EarthMap like previously val em = new PetersEarthMap() nor with val em = new PetersEarthMap.

Ah – yeah, subclassing a highly-parameterized class can be a headache. As you’re finding, you need to duplicate and pass through the parameters that you will need, including their defaults.

Not sure what the ideal approach here is; it sort of depends on the code. I might consider breaking the class down into components, to make the project and invProject pluggable, rather than subclassing – there’s a common truism in modern OO programming of “favor composition instead of inheritance”. Or I might think about whether a typeclass is appropriate for project and invProject, with multiple implementations for the different versions…