1

I want to have a case class extend a trait

Here are my requirements:

  1. I need to use a case class for the child. This is a hard requirement because of scopt (https://github.com/scopt/scopt)
  2. The parent needs to be a trait. Apologies for not being clear about it earlier
  3. I want the attributes of the parent trait to be accessible to the child, and preferrably not have to specify them again when declaring the child case class. For example, if the parent trait is: trait ParentParams { var name: String = "Al" } then the child should not be case class ChildParams(id: String = "whatever", override val name: String = Parent.defaultName) extends Parent. I would prefer to have it as case class Child(id: String) extends Parent. However, when I try the second approach, I am not able to access the name field when I try to do a .copy() on a case class object. As a workaround I can mutate the value of the name attribute through childObj.name = "newName"
  4. I should (hard requirement) be able to override the attribute values that the child object inherited from the parent. Right now this is possible because I have declared the parent attributes as a var instead of a val. However, ideally I would like to make it immutable and use the copy method for changing values instead.

This is what I have right now, but am unable to use the .copy() method to change the value of an inherited attribute.

package abcd.wxyz

import org.scalatest.{FlatSpec, Matchers}

trait ParentTrait {
  var name: String = "name"
}

case class TestParams(param1: String = "123") extends ParentTrait

class TestParamsSpec extends FlatSpec with Matchers {
  "TestParams" should "be able to access it's inherited attributes and modify them" in {

    val testParams = TestParams()
    testParams.name = "newName1"
    testParams.param1 should equal("123")
    testParams.name should equal("newName1")

    val modifiedTestParams = testParams.copy(name = "newName2") // cannot resolve symbol name
    modifiedTestParams.name should equal("newName2")

  }
}

I get a "Cannot resolve symbol name" on .copy(name = "newName2") for the code above.

The reason behind having this inheritance is to have a function which accepts a Child object and expects it to have a "name" attribute defined. For example, consider a function which will append "Mr." or "Mrs." given a Child object having a name attribute.

Overall background of what is happening: I am filling a config with command line input values (into a case class) using scopt. If no value is provided, a default is used for that attribute. By attribute I mean the data member inside the case class. I have a function (let's call it dbReader) which accepts an object of the case class and uses it to establish a connection and read from a database. There are many different parameter case classes in my project and I want each of these parameters to implement a common DatabaseConnectionParameters trait so that the function dbReader can work regardless of which parameter case class is passed to it.

Omkar Neogi
  • 675
  • 2
  • 9
  • 30
  • 1
    I do not get your problem. How do you expect this to work? `case class Child(id: String = "55", override val name: String = super.name)` it is not extending **Parent**, and even if it would do, how do you expect to use the `name` of your parent, if you need to pass that name when constructing? - I believe, this is an **XY Problem**, try to reformulate your question to represent your real problem, and provide all the info you can. Is `Parent` in your control?, can you refactor it? – Luis Miguel Mejía Suárez May 28 '19 at 15:29
  • Not writing the `extends Parent` in the first code block was a mistake which I now fixed. I have access to both the Parent and Child classes and can refactor them. I am updating the question to reexplain the problem. – Omkar Neogi May 28 '19 at 15:40
  • Omkar sorry, I still do not understand. First your first block still does not compile, and again it does not make sense, you have to pass `name` to the constructor, what is the meaning of the override with a default value which does no exists? - Second, _"Child object to have a "name" attribute defined"_ if child defines `name` it has name, it does not need to override nothing, unless you want to define a function that accepts parent an you want to pass a child. – Luis Miguel Mejía Suárez May 28 '19 at 15:55
  • Third, _"which may or may not override the attributes of the trait/class "_, if the field is abstract, you have to define, if both are concrete, you have to override. If the parent is concrete, and child does not have it, it will inherit, you do not have to define nothing in child. - Let me refrase your problem to see if I get it, you want to fill a config, from the parameters, and if some of them does not exists use a default. Is that right? – Luis Miguel Mejía Suárez May 28 '19 at 15:57
  • In response to the "third" comment, yes, I want to fill a config. If some value does not exist, scopt keeps the default value from the case class. I agree to this, but the problem is the case class inheriting the attributes of the parent class/trait. Regarding the "second" comment, I do need the functionality of having a function accept a parent and me passing a child. However, whatever I have tried to do, I have not been able to get the code to even compile. – Omkar Neogi May 28 '19 at 16:21
  • Omkar, Why do you even need the inheritance? Why you do not simple define the case class with the defaults? Because, I think I now understand, that your problem is that you want to provide the defaults from your parent, right? – Luis Miguel Mejía Suárez May 28 '19 at 16:24
  • Yes, I would not want to define the defaults both in the child and in the parent. – Omkar Neogi May 28 '19 at 16:25
  • And you are absolute sure you need the parent? If so, please rewrite the question explaining that problem and provide more examples of what you need. – Luis Miguel Mejía Suárez May 28 '19 at 16:28

2 Answers2

6

In scala, a default value for a constructor is compiled as a function which evaluates in the context of the companion object, so

case class CaseClazz(foo: String = super.bar) extends Bar

Compiles to something like (I'm not mangling the names in the interest of clarity):

class Bar { def bar: String = "whatevs" }

case class CaseClazz(foo: String = CaseClazz.defaultForFoo) extends Bar

object CaseClazz extends Function1[String, CaseClazz] {
  def defaultForFoo: String = super.bar
  def apply(foo: String = defaultForFoo): CaseClazz = new CaseClazz(foo)
}

Which doesn't compile because super in the object is CaseClazz$, which doesn't define a bar method.

The clearest way to get the behavior you seek is IMO:

object Parent {
  val defaultName: String = "Al"
}

class Parent(val name: String = Parent.defaultName) { def parentBehavior: Unit = println("parent!") }

case class Child(id: String = "55", override val name: String = Parent.defaultName) extends Parent(name)

Which allows:

scala> Child(id = "ego").parentBehavior
parent!

scala> Child(id = "ego").name
res12: String = Al

scala> Child().name
res13: String = Al

scala> Child().parentBehavior
parent!
Levi Ramsey
  • 18,884
  • 1
  • 16
  • 30
  • I found your solution useful but am not happy having to write the `override val name: String = Parent.defaultName` once again in the Child since I am inheriting the Parent class. An ideal solution would not require having to specify the `name` attribute once again in the child case class declaration but at the same time, have all the parent/inherited attributes available when calling methods like copy on those inherited attributes. Could you please share how your solution would be modified for that? Also, I plan to use a trait instead of a class for the parent. Earlier I wasn't sure about this – Omkar Neogi May 31 '19 at 19:48
0
case class Child(id: String = "55", override val name: String = super.name) extends Parent

Does not work, but

 case class Child(id: String = "55", override val name: String) extends Parent(name)

does ...

You have to provide a constructor parameter to Parent to be able instantiate it.

Dima
  • 39,570
  • 6
  • 44
  • 70