Why don't subclasses inherit initialisation arguments from parents?

Is there a way top make a subclass inherit the initialisation arguments of its parent class?

For example, If I can construct an object of class A using new A(x=3,y=4) and B is a subclass of A, I’d like to also contract a B as new B(x=3,y=4).

In Scala it seems to the the way to do that is to write a lot of boiler plating in the class definition of B.

Here is what I’ve written. It seems like too much work and error prone for something so simple. I.e., I need to duplicate all the default values for every subclass, and I need to duplicate all the parameters with the extends EarthMap(...) syntactical element. But all I really want to to is create several subclass which override two particular methods.

class PetersEarthMap(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,
                     legend:Boolean = false,
                     palette:ColorPalette = ColorPalette.defaultTemperatureColorPalette)
  extends EarthMap(locationLL=locationLL,locationUR=locationUR,
    width=width,height=height,borders=borders,legend=legend,palette=palette) {

  // 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)
}

class SimpleEarthMap(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,
                     legend:Boolean = false,
                     palette:ColorPalette = ColorPalette.defaultTemperatureColorPalette)
  extends EarthMap(locationLL=locationLL,locationUR=locationUR,
    width=width,height=height,borders=borders,legend=legend,palette=palette) {

  // simple projection
  def project(loc: Location):(Double,Double) = (loc.lat -> loc.lon)
  def invProject(lat: Double, lon: Double):Location = Location(lat,lon)
}

abstract 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,
                        legend:Boolean=false,
                        palette:ColorPalette = ColorPalette.defaultTemperatureColorPalette) {
  val Location(latMin: Double, lonMin: Double) = locationLL
  val Location(latMax: Double, lonMax: Double) = locationUR

  // projection
  def project(loc: Location):(Double,Double)
  def invProject(lat: Double, lon: Double):Location

 /// stuff omitted
}

Here is what I’d like to write to create two subclasses which inherit the initialisation behaviour of the superclass.

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

class SimpleEarthMap  extends EarthMap {

  // simple projection
  def project(loc: Location):(Double,Double) = (loc.lat -> loc.lon)
  def invProject(lat: Double, lon: Double):Location = Location(lat,lon)
}

Maybe you may want to split the logic from doing projections from the data of representing a Map / Location.

1 Like

Not sure I follow?

To me it seems really like a weakness in the object model of Scala that how-to-initialisation does not inherit. Perhaps this weakness is inherited from Java, and nobody noticed because they haven’t used more powerful (or more programmer-friendly) object systems than Java before coming to Scala?

on the other hand, perhaps this missing feature should motivate me to learn scala macros and meta programming.

Does anyone know whether it is possible to write a macro which will allow me to type something like

my_class PetersEarthMap extends EarthMap {...}

and have the compiler see the original code about which redundantly passes all the initialisation arguments along? After all it is just a syntactical transformation. right?

class PetersEarthMap(locationLL:Location=Location(-90,-180),
                     locationUR:Location=Location(90,180), 
                     width:Int=360, height:Int=180, 
                     borders:Boolean = true,
                     legend:Boolean = false,
                     palette:ColorPalette = ColorPalette.defaultTemperatureColorPalette)
  extends EarthMap(locationLL=locationLL,locationUR=locationUR,
    width=width,height=height,borders=borders,legend=legend,palette=palette) {
...
}

To me it seems really like a weakness in the object model of Scala that how-to-initialisation does not inherit.

Does it work in other languages? I really do not too much about OOP.
But AFAIK constructors are actually weird and are not inherited, because they are not methods, they are not functions, they are something completely different.
Note that I agree I would like them not to be that different for normal methods, but maybe it is a limitation from Java interop.

Does anyone know whether it is possible to write a macro which will allow me to type something like

Yes, it would be totally possible, but I would really stay away of macros unless there is no other way.

Not sure I follow?

Basically, decouple your model.

final 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,
    legend: Boolean = false,
    palette: ColorPalette = ColorPalette.defaultTemperatureColorPalette
)

sealed trait Projection {
  // I really do not understand why this was inside the Map if it doesn't use it.
  def project(map: EarthMap)(loc: Location): (Double, Double)
  def invProject(map: EarthMap)(lat: Double, lon: Double): Location
}

object Projection {
  def simple: Projection new Projection {
    override def project(map: EarthMap)(loc: Location):(Double,Double) =
      (loc.lat -> loc.lon)

    override def invProject(map: EarthMap)(lat: Double, lon: Double): Location =
      Location(lat,lon)
  }

  def peters: Projection new Projection {
    override def project(map: EarthMap)(loc: Location):(Double,Double) =
      (90 * sin(toRadians(loc.lat)) -> loc.lon)

    override def invProject(map: EarthMap)(lat: Double, lon: Double): Location =
      Location(toDegrees(asin(lat / 90)), lon)
  }
}
3 Likes

It’s not a weakness, so much as it’s a different way of thinking about this stuff. @BalmungSan’s example illustrates this. In classic OO, yes, it’s normal to totally conflate the data and the operations: to think of everything in terms of inheritance. But in Scala, it’s more common to think about it the way he’s doing – in particular, if the implementation of the operations can vary, it is very, very common to separate the concepts – to put the data in one place and the operations in another.

The language even reflects that bias, in that case classes (Scala’s standard way of representing data structures) don’t permit easy inheritance. Methods on case classes should usually be immutable and obvious derivations from the constructor parameters.

2 Likes

Why don’t subclasses inherit initialisation arguments from parents?

Because parent classes do their part of object’s initialization, including doing some real work (pure or impure computation) and setting private fields (accessible only to code within parent class). Also a common idiom is a situation where a subclass fixes some params so you don’t have to provide them at all when creating subclass, i.e. when you have class SubClass extends SuperClass(0, "a", 'c', 5.0, Abc(1)) then it’s enough to do new SubClass() to create an instance of SubClass.

Constructors are a special type of static methods used to initialize an uninitialized object. When invoking new instruction, JVM first allocates and zeroes memory for that object and then invokes its constructor. Since calling a parent constructor at the beginning of child constructor is required by Java specification, you can rely on that, i.e. you can rely on fact that parts of object inherited from parent class are already initialized when running child constructor.

Having a class or metod with too many parameters or passing them over and over is an anti-pattern in general. If the parameter list grows too big then grouping the parameters into objects is preferred. This is called https://en.wikipedia.org/wiki/Object_composition

Aside from composition a pattern in Scala is to use base trait with implementing case classes, eg:

sealed trait MyType { // base trait
  def param1: Type1
  def param2: Type2
  ...
  def paramN: TypeN

  def someMethod(argX: TypeX, argY: TypeY): TypeZ
}

object MyType {
  case class TypeA(param1: Type1, param2: Type2, ..., paramN: TypeN) extends MyType {
    def someMethod(argX: TypeX, argY: TypeY): TypeZ = ???
  }
  case class TypeB(param1: Type1, param2: Type2, ..., paramN: TypeN) extends MyType {
    def someMethod(argX: TypeX, argY: TypeY): TypeZ = ???
  }
  case class TypeC(param1: Type1, param2: Type2, ..., paramN: TypeN) extends MyType {
    def someMethod(argX: TypeX, argY: TypeY): TypeZ = ???
  }
}

You could also move the functions into case class (primary or secondary) constructor, but then you could have problems with equality between instances or at least it would be tricky to tell when equality on functions works as expected and when it doesn’t.

2 Likes

I’m trying to understand the idea of making the Projection into a trait. But how does the map know the projection in this case? I.e., the map rendering code needs access to the project and invProject methods in order to compute to and from pixel locations in the rendered image.

I’m not sure it makes sense to have an EarthMap who doesn’t have a projection.

Yes, that seems to be the case. Scala inherits lots of baggage from Java. People who know Java perhaps find it normal. People coming from elsewhere may find it a bit arcane.

In case anyone is interested:

In CLOS instance initialization is done when the system calls a particular method called initialize-instance. Any class may define such a method if it needs to contribute something to the initialization of an instance. Normally, classes define such methods with an :after qualifier, specifying that it is called after the primary method returns. Doing so allows the method to assure that any initialization done by the super classes has finished.

Similarly if an initialize-instance :before is defined for a class, that method is called before the primary method, thus before the superclass has a chance to initialize. This is sometimes useful to signal errors if some precondition is not met.

Importantly, this (before/after/around/primary) behavior is in no way special for constructors, rather it is a feature of methods in general.

Aha!

Because a Projection doesn’t need a Map a Map needs a projection.
So you may model that like this:

sealed trait Projection {
  def project(loc: Location): (Double, Double)
  def invProject(lat: Double, lon: Double): Location
}

final 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,
    legend: Boolean = false,
    palette: ColorPalette = ColorPalette.defaultTemperatureColorPalette,
    projection: Projection
)

Or if you need to distinguish maps by their projections, you can do this:

final case class EarthMap[P <: Projection](
    ...
    projection: P
)

Also, maybe instead of the map having a projection. Maybe, the methods on the map that uses the projection, could receive it as an argument. Or maybe, there could be another entity that consists of a map and a projection.

All of those alternatives seem better than inheritance.

1 Like

I think you’ve mixed up quotations. Anyway, every time your expectations surprise me. I’ve learned quite a few popular languages and none of the currently popular ones has features from CLOS that you’ve mentioned. If Scala had them then almost all people coming to Scala would find them at least “a bit arcane”.

Java has somewhat similar mechanics to C++ as it was marketed at C++ programmers at first. C# has similar mechanics to Java as it was a direct Microsoft’s answer to Sun’s Java.

2 Likes

Agreed. CLOS is a nice system (I actually built the first IDE for Ada '95 in a combination of that and Emacs Lisp, way back when), but it’s an outlier linguistically – most OO languages are significantly more like Scala than they are like CLOS…

1 Like

I try to tell my students that they should learn many different approaches, not just one. Admittedly its hard to do so without forming a subjective opinion. For example, on the subject of writing functional style code (function manipulation, higher order functions, immutability) I tell them that its not better or worse, but rather a philosophy, and learning that approach will make them better programmers in general, because they’ll have a larger repertoire of techniques for use in whatever language they end up using in the industry.

Must have been a fascinating project. Bravo!

Thanks BalmungSan, I’m trying to incorporate your advise into my program…

Can you elaborate on a point I didn’t really understand. Why do I need [P <: Projection] and then declaring projection:P rather than just declaring projection:Projection ?

Here is what I have done which seems to work. But maybe there’s a bug hidden that I don’t yet see.

sealed abstract class Projection {
  // project is a function which takes a location on the globe, and reprojects it to
  //   a deformation of the glob.  The return value (lat,lon) of project is assumed to be
  //   interpretable as a location.  I.e., -90 <= lat <= 90 and -180 <= lon < 180
  def project(loc: Location):(Double,Double)

  // invProject is the inverse function of project
  def invProject(lat: Double, lon: Double):Location
}

object PetersProjection extends Projection {
  import scala.math._
  def project(loc: Location): (Double, Double) = (90 * sin(toRadians(loc.lat)) -> loc.lon)
  def invProject(lat: Double, lon: Double): Location = Location(toDegrees(asin(lat / 90)), lon)
}

object SimpleProjection extends Projection {
  def project(loc: Location):(Double,Double) = (loc.lat -> loc.lon)
  def invProject(lat: Double, lon: Double):Location = Location(lat,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,
                    legend:Boolean=false,
                    projection:Projection = SimpleProjection,
                    palette:ColorPalette = ColorPalette.defaultTemperatureColorPalette) {
 ... stuff omitted ...
}

I’ve refactored my program to take your advise, more or less, as far as I understand it.
I wonder if you’d (BalmungSan or anyone) would like to take a look at the project.
It’s a work in progress, available as read-only on a local gitlab. You can clone but not fork.
The interesting part is the globe package in the directory of the same name
src/main/scala/globe
scala globe project and more

It’s a matter of type strength, mostly. The [P <: Projection] version bakes the knowledge of which Projection this EarthMap uses in at the type level, not just the value level. That makes somewhat more power available to you – typeclasses that depend on that parameter, and things like that, and means that you can distinguish the two different kinds of maps so that they can’t be mixed up – so that you can have vals that are specifically an EarthMap[PetersProjection], and can’t accidentally put an EarthMap[SimpleProjection] in there.

It’s not required to do this sort of thing, and it might or might not have any benefit in your situation. But a general rule of idiomatic Scala is Stronger Types Are Usually Better: you often discover, down the line, that your life would have been easier if you had made your types more precise. So it’s good practice to at least think about this sort of thing…

1 Like

As @jducoeur said, it is really not a requirement.
On your original version with subtyping, you could distinguish two different maps by their projections. I just showed you how to do that using my approach, in case you needed that; nothing more nothing less.