Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When does it make sense to use tuples over case class

Tags:

scala

case class Person(firstName: String, lastName: String)

vs

type Person = (String, String) //firstName, lastName

case classes are obviously more readable. However, I have seen people use the latter. When would it make sense to use a tuple instead?

As a follow up

Are there performance difference? In some langauges using a Tuple2 would be just like using two primitives, while creating a class would have an overhead. But is Scala is seems like Tuple is implemented as class. How about a situation where I want to zip two sequances, but would prefer the output to be a case class for all the reasons mentioned bellow.

For example

`case class UserScores(userId: Long, score: Double)`
users.zip(scores).map{(id ,score) => UserScores(id,score)}

The above example requires an extra iteration over the collection, plus extra object creation. One could use a view instead

users.zip(scores).view.map{(id ,score) => UserScores(id,score)}.force

But I am not quite sure it will have the intended effect

like image 465
EugeneMi Avatar asked Mar 01 '18 16:03

EugeneMi


2 Answers

You should use tuples when it is the most appropriate level of abstraction.

For example, if you are writing a framework where everything is completely generic, and the most concrete things that you can get are "functions", "cartesian products", "semigroupals", or something like "closed monoidal categories", then you probably will have no other choice but using Tuple. Here is one good example: Semigroupal with method:

 product[A, B](fa: F[A], fb: F[B]): F[(A, B)]

Here, the usage of the tuple type is completely justified, because the interface is sufficiently abstract.

Another example: in the entire Scala collection library, you will never encounter anything as concrete as Person(name: String, surname: String), but instead you will see a huge lot of methods that work on tuples of generic types (A, B).

However, if the most abstract thing in your code is

object Fred {
  val name = "Fred"
  val surname =  "Bloggs"
}

and then you suddenly find out that you also need another similar object for John Doe, it would be much more appropriate to define

case class Person(name: String, surname: String)

and then maybe even to use it like this:

val j = Person(name = "John", surname = "Doe")

Of course, there is great value in accurately modeling the domain. These two goals -- accurately modeling a concrete domain vs. writing generic frameworks that work in as many cases as possible -- are complementary.

To summarize:

  • Are you writing a sufficiently abstract completely generic framework? Use tuples.
  • Are you trying to model some concrete domain as closely as possible? Use appropriately named case classes.

This here:

type Person = (String, String) //firstName, lastName

looks like a mismatch between the domain and the used abstraction. You can see it simply from the fact that the compiler does not prevent you from accidentally swapping the two components. In contrast, if you worked at a different abstraction level with generic A and B types, the compiler would emit an error message if you accidentally swapped (B, A) where an (A, B) was expected.

like image 134
Andrey Tyukin Avatar answered Oct 20 '22 00:10

Andrey Tyukin


IMHO, as a general rule:

  • By default, you should prefer case class: it doesn't cost more and it is much clearer
  • Also, case class provides lots of practical functionalities, such as apply, unapply and copy (from comment: tuples also have those)
  • And tuples of length > 3 should be avoided in general

But:

  • For methods that have several outputs, which are typically split right-away. Then use tuple
  • In several algorithms, especially using functional map/flatMap/etc..., you have lot's of intermediate "one-line" results for which you don't need to define a case class.
  • Finally, as stated by andrey-tyukin, if you're coding generic-type algorithms you could prefer tuples. But then, you can actually do nice generic case-class code using shapeless magic

A last comment, concerning type alias: I use those mainly to improve readability when I don't want to create classes. This happens in two cases:

  • the aliased type is a primitive, such as Int or Double, that is used in critical code and I want to avoid boxing them (note that this requires to respect other constraints to be effective)
  • when a single type has many meanings but creating a case class would be over-design. Ex. say several APIs use param: Map[String, Double]. Map is a good type and you don't want to wrap it in a class. Then it may be more readable to have ApiParams in your code where type ApiParams = Map[String, Double]
like image 29
Juh_ Avatar answered Oct 19 '22 23:10

Juh_