igx
igx

Reputation: 4231

case class with logic what is the idiomatic way

What is the FP idiomatic way for this: let's say I have this

trait Name

object Name{
 def apply(name: String): Name = {
     if (name.trim.isEmpty || name.trim.length < 3)
       InvalidName
     else 
       ValidName(name.trim)
    }
 }

case object InvalidName extends Name
case class ValidName(name:String) extends AnyVal with Name

Now I have some helper functions such as

def split = name.splitAt(" ")
//some more functions 

which way is more idiomatic:

  1. Put them in the case class it self but that some how makes the case class to contain some logic however I can do something like :

    val n = ValidName("john smith")

    val (first, last) = n.split

  2. Put them in dedicated object but then the method will look like

    def split(n: ValidName) = n.name.splitAt(" ")

  3. Create an object with implicit class that will accept Name and will call the methods

What do you think ?

Upvotes: 2

Views: 761

Answers (3)

Alvaro Carrasco
Alvaro Carrasco

Reputation: 6182

More idiomatic:

case class Name private (name: String) {
  lazy val first :: last :: Nil = name.split(" ").toList
}
object Name {
  def fromString (name: String): Either[String, Name] = {
    if (name.trim.isEmpty || name.trim.length < 3) Left("Invalid name")
    else Right(new Name(name.trim))
  }
}

Or maybe this:

case class Name (first: String, last: String) {
  lazy val fullName = s"$first $last"
}
object Name {
  def fromString (name: String): Either[String, Name] = {
    if (name.trim.isEmpty || name.trim.length < 3) Left("Invalid name")
    else {
      val first :: last :: Nil = name.split(" ").toList
      Right(new Name(first, last))
    }
  }
}

In scala it's more idiomatic to represent failure cases through the use of Either than through inheritance. If you have an instance of N, you can't call any functions on it, you'll probably have to pattern match it. But a type like Either already comes with functions like map, fold, etc. that makes it easier to work with.

Having a private constructor helps ensure that you can only create a valid Name because the only way to create one is through the fromString method.

DO NOT use implicits for this. There's no need and would only make for confusing code. Not really what implicits are for.

Upvotes: 2

Tim
Tim

Reputation: 27356

There is no major issue with adding logic to a case class, especially when it is just extracting the data in a different format. (It becomes problematic when you add data members to a case class).

So I would do option 1 and not worry about it!


In response to the comments, case class is actually just a shortcut for creating a class with a whole bunch of useful pre-implemented methods. In particular, the unapply method allows a case class to be used in pattern matching, and equals does an element-wise comparison of the fields of two instances.

And there are a bunch of other methods to extract the data from the case class in different ways. The most obvious are toString and copy, but there are others like hashCode and all the stuff inherited from Product, such as productIterator.

Since a case class already has methods to extract the data in useful ways, I see no objection to adding your split method as another way of extracting data from the case class.

Upvotes: 2

Jeremy
Jeremy

Reputation: 1924

I think it depends on context. In this case, if most of the methods you are using are just slight tweaks to String methods, you might want to consider a fourth option:

case class Name(name: String)
implicit def NameToString(n: Name) = n.name

Name("Iron Man").split(" ") // Array(Iron, Man)

Upvotes: 0

Related Questions