Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to initialize one field using another in case class?

Suppose I have a case class like

case class Person(fname:String, lname:String, nickName:Option[String] = None)

On creating an instance like Person("John", "Doe"), I want nickName to be automatically assigned to fname, if one is not given. Eg:

val p1 = Person("John", "Doe")
p1.nickName.get == "John"

val p2 = Person("Jane", "Doe", "Joe")
p2.nickName.get == "Joe"

How can auto assignment of one field from another field be achieved?

Trying the solutions below in repl. I think this is something to do with repl

scala> case class Person(fname: String, lname:String, nickName:Option[String])
defined class Person

scala> object Person { def apply(fname:String, lname:String):Person = {Person(fname, lname, Some(fname))}}
  console:9: error: too many arguments for method apply: (fname: String, lname: String)Person in object Person
   object Person { def apply(fname:String, lname:String):Person = {Person(fname, lname, Some(fname))}}
like image 616
scout Avatar asked Jan 27 '15 20:01

scout


3 Answers

Technical Solution (don't)

On the current definition of case classes, you can override the constructor of the case class and the apply method of its companion object, as described in the answer of Facundo Fabre.

You will get something like this:

object Person {
  def apply(fname:String, lname:String): Person = Person(fname, lname, fname)
}

case class Person(fname:String, lname:String, nickName: String) {
  def this(fname:String, lname:String) = this(fname, lname, fname)
}

This is technical correct and quite clever coding. But for my taste its a little bit too clever, because it breaks an important property:

CaseClass.unapply(CaseClass.apply(x1,x2,x3)) == (x1,x2,x3)

In other words: When I construct a case class using apply with some tuple of parameter and then deconstruct it using unapply I expect to get the original tuple I put into apply (ignoring currying and option type).

But in this case, this property is not true any more:

Person.unapply(Person("John", "Smith"))
// result: Option[(String, String, String)] = Some((John,Smith,John))

Deconstruction using unapply is used for pattern matching (match{ case ... => ...). And this is a common use case for case classes.

So while the code is quite clever, it might confuse other people and break properties their code relies on.

Rephrase the problem (my suggestion)

When overly clever code is needed, it is often a good idea to rethink, what problem one tries to solve. In this case, I would suggest to distinguish between the nick name the user has chosen and a nick the system assigns to the user. In this case I would then just build a case class like this:

case class NickedPerson(fname : String, lname : String, chosenNick : Option[String] = None) {
  val nick = chosenNick.getOrElse(fname)
}

You can then just use the field nick to access the computed nick name, or use chosenNick if you want to know if the user has provided that nick name:

NickedPerson("John", "Smith").nick
// result: String = "John"
NickedPerson("John", "Smith", Some("Agent")).nick
// result: String = "Agent"

The basic properties about case classes are not changed by this code.

like image 169
stefan.schwetschke Avatar answered Oct 20 '22 14:10

stefan.schwetschke


You can overload the constructor of the case class

case class Foo(bar: Int, baz: Int) {
     def this(bar: Int) = this(bar, 0)
}
new Foo(1, 2)
new Foo(1)

So you can check the case if nickName is none.

You can also overload it's apply method in the same way. In that way, then you can use

Foo(1,2)
Foo(1)
like image 27
Facundo Fabre Avatar answered Oct 20 '22 14:10

Facundo Fabre


Just explaining how to overload apply from companion object (in addition to @Facundo Fabre answer):

scala> :paste
// Entering paste mode (ctrl-D to finish)

object Person {
   def apply(fname:String, lname:String): Person = Person(fname, lname, fname)
}
case class Person(fname:String, lname:String, nickName: String)


// Exiting paste mode, now interpreting.

defined object Person
defined class Person

scala> Person("aaa", "bbb")
res24: Person = Person(aaa,bbb,aaa)

scala> Person("aaa", "bbb", "ccc")
res25: Person = Person(aaa,bbb,ccc)

You could also define default value using muti-parameter list constructor, but it's hard to use such case class (no toString and pattern matching for last parameter), so won't recommend (but it's good if you want simmilar thing for methods):

scala> case class Person(fname:String, lname:String)(val nickName: String = fname)
defined class Person

Another funny solution (just to play), which I wouldn't recommend to use in real code:

scala> case class Person(fname:String, lname:String, var nickName: String = null){nickName = Option(nickName).getOrElse(fname)}
defined class Person

scala> Person("aaa", "bbb")
res32: Person = Person(aaa,bbb,aaa)
like image 43
dk14 Avatar answered Oct 20 '22 15:10

dk14