1

I'm struggling to understand if it possible for a apply function to return an instance of a specific class. Here is an example:

I have an abstract trait

trait BaseTrait {
  def add(s: Int): Int
}

and two classes that inherit this trait and:

  1. override an abstract method
  2. specify a method another_method
class Child1 extends BaseTrait {

  override def add(s: Int): Int = {
    s + s
  }

  def another_method(): Unit = {
    println("Another method in Child1")
  }
}

class Child2 extends BaseTrait {

  override def add(s: Int): Int = {
    s + s
  }

  def another_method(): Unit = {
    println("Another method in Child2")
  }
}

When I try to access the contents of child classes from a utility function in a Base Trait

object BaseTrait {
  def apply(t: String): BaseTrait = {
    t match {
      case "one" => new Child1()
      case "two" => new Child2()
    }
  }
}

val child2 = BaseTrait("one")

i don't see another_method method

child2.another_method() // cannot resolve symbol another method

Question: is there a way in Scala (generics/lower-upper bounds) to configure an apply method in such a way that it returns an instances of classes where it will be possible to access both overridden abstract methods AND methods belonging to this exact class (not just those that are defined in BaseTrait)

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
Eugene
  • 53
  • 1
  • 5
  • 3
    You need to understand the difference between compile-time types and runtime classes. Yes, the returned value of `BaseTrait("one")` is an instance of the **Child1** class, but it is statically typed as a **BaseTrait**; so for the compiler, it doesn't have the `another_method` method, since **BaseTrait** doesn't define it. - There is no way to change that, given the returned class depends on runtime information, for example assume this `BaseTrait(str)` where str is read from a file, how do you know which class it is going to be? you can't. - This is the core idea of abstraction and polymorphism – Luis Miguel Mejía Suárez Aug 09 '20 at 14:07
  • Thank you! And there is no way to use advanced typing techniques to make this possible, correct? – Eugene Aug 09 '20 at 14:14
  • 2
    It depends. Is the family of types closed? Then you can use **pattern matching**. Or if the information doesn't come from runtime but rather compile time constants, there are tricks like Mario show. You can also rethink you solution so you can get the correct type. Or you can just throw a dirty `asInstanceOf` if you are totally sure what the class is going to be _(spoiler alert, this is usually a really bad idea)_. - So, in other words, if you want a particular solution for a particular problem then describe it with all the details, but a general answer is no, this is usually not possible. – Luis Miguel Mejía Suárez Aug 09 '20 at 14:18
  • The approach I'm describing is taken from [this article](https://towardsdatascience.com/write-clean-and-solid-scala-spark-jobs-28ac4395424a). Particularly, you can look at the code in this [gist](https://gist.github.com/ronald-smith-angel/eb9bab8a1e029eadf96ec92cff6dffd4#file-cleanerservice-scala). – Eugene Aug 09 '20 at 14:43
  • The main idea is to have a BaseTrait implementing a base logic for all cleaning (or enrichment) transformations for a single source in a single Spark pipeline. But the problem comes when you have multiple sources (therefore multiple classes that inherit a BaseTrait) that require additional logic. I don't want this additional logic (addition method) to reside in a base trait. I want them to be in their respective classes (Child1, Child2 in our example). – Eugene Aug 09 '20 at 14:46
  • Yeah, that is ok, but why do you want to instantiate them using a **String**? – Luis Miguel Mejía Suárez Aug 09 '20 at 14:48
  • 3
    @Eugene why not to abstract over any cleaning with a single `clean` method? why do you need different types in the base trait and some additional in others? – Artem Sokolov Aug 09 '20 at 14:58
  • @LuisMiguelMejíaSuárez it is an implementation i try to follow without any concern about whether it is reasonable or not :) Is there a better way to instantiate them? – Eugene Aug 09 '20 at 15:23
  • @ArtemSokolov I think the main idea is to have a container with base cleaning logic for all sources that is "closed for modification and open for extension". But if I want a custom cleaning logic per Spark source (i.e. per class extending base trait) I have no way to do it. I can only define ALL methods in a single base trait and then all classes will have to implement ALL of these methods which seems unreasonable. – Eugene Aug 09 '20 at 15:28
  • 1
    Why not just call the constructor of the class you want? – Luis Miguel Mejía Suárez Aug 09 '20 at 15:44
  • 1
    @LuisMiguelMejíaSuárez yes, it seems like the most reasonable thing to do – Eugene Aug 09 '20 at 15:58
  • 1
    @Eugene nope, I thoght about something different. If your service is about clean - just make base 1 pulic method `clean(...)` and realize it in each subtype correspondingly. This is a design that corresponds to SOLID. If you want to reuse common logic you can use a static object, or helpers, or other services that will be injected in services that need to share common logic. If you need to struggle through types - it probably is a bad design and abstractions. – Artem Sokolov Aug 09 '20 at 17:11

2 Answers2

8

In this particular case you could try literal types

object BaseTrait {
  def apply(t: "one"): Child1 = new Child1()
  def apply(t: "two"): Child2 = new Child2()
}

val child1 = BaseTrait("one")
child1.another_method()
// Another method in Child1
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
4

Besides overloading you can also try a type class

object BaseTrait {
  def apply[S <: String with Singleton](t: S)(implicit tc: MyTypeclass[S]): tc.Out = tc()
}

trait MyTypeclass[S <: String] {
  type Out <: BaseTrait
  def apply(): Out
}
object MyTypeclass {
  implicit val one = new MyTypeclass["one"] {
    override type Out = Child1
    override def apply() = new Child1()
  }

  implicit val two = new MyTypeclass["two"] {
    override type Out = Child2
    override def apply() = new Child2()
  }
}

val child2 = BaseTrait("one")

child2.another_method()

(Scala 2.13)

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66