Search code examples
scalaiterable-unpacking

Unpacking tuple directly into class in scala


Scala gives the ability to unpack a tuple into multiple local variables when performing various operations, for example if I have some data

val infos = Array(("Matt", "Awesome"), ("Matt's Brother", "Just OK"))

then instead of doing something ugly like

infos.map{ person_info => person_info._1 + " is " + person_info._2 }

I can choose the much more elegant

infos.map{ case (person, status) => person + " is " + status }

One thing I've often wondered about is how to directly unpack the tuple into, say, the arguments to be used in a class constructor. I'm imagining something like this:

case class PersonInfo(person: String, status: String)
infos.map{ case (p: PersonInfo) => p.person + " is " + p.status }

or even better if PersonInfo has methods:

infos.map{ case (p: PersonInfo) => p.verboseStatus() }

But of course this doesn't work. Apologies if this has already been asked -- I haven't been able to find a direct answer -- is there a way to do this?


Solution

  • I believe you can get to the methods at least in Scala 2.11.x, also, if you haven't heard of it, you should checkout The Neophyte's Guide to Scala Part 1: Extractors.

    The whole 16 part series is fantastic, but part 1 deals with case classes, pattern matching and extractors, which is what I think you are after.

    Also, I get that java.lang.String complaint in IntelliJ as well, it defaults to that for reasons that are not entirely clear to me, I was able to work around it by explicitly setting the type in the typical "postfix style" i.e. _: String. There must be some way to work around that though.

    object Demo {
    
       case class Person(name: String, status: String) {
          def verboseStatus() = s"The status of $name is $status"
       }
    
       val peeps = Array(("Matt", "Alive"), ("Soraya", "Dead"))
    
       peeps.map {
         case p @ (_ :String, _ :String) => Person.tupled(p).verboseStatus()
       }
    
    }
    

    UPDATE:

    So after seeing a few of the other answers, I was curious if there was any performance differences between them. So I set up, what I think might be a reasonable test using an Array of 1,000,000 random string tuples and each implementation is run 100 times:

    import scala.util.Random
    
    object Demo extends App {
    
      //Utility Code
      def randomTuple(): (String, String) = {
        val random = new Random
        (random.nextString(5), random.nextString(5))
      }
    
      def timer[R](code: => R)(implicit runs: Int): Unit = {
        var total = 0L
        (1 to runs).foreach { i =>
          val t0 = System.currentTimeMillis()
          code
          val t1 = System.currentTimeMillis()
          total += (t1 - t0)
        }
        println(s"Time to perform code block ${total / runs}ms\n")
      }
    
      //Setup
      case class Person(name: String, status: String) {
        def verboseStatus() = s"$name is $status"
      }
    
      object PersonInfoU {
        def unapply(x: (String, String)) = Some(Person(x._1, x._2))
      }
    
      val infos = Array.fill[(String, String)](1000000)(randomTuple)
    
      //Timer
      implicit val runs: Int = 100
    
      println("Using two map operations")
      timer {
        infos.map(Person.tupled).map(_.verboseStatus)
      }
    
      println("Pattern matching and calling tupled")
      timer {
        infos.map {
          case p @ (_: String, _: String) => Person.tupled(p).verboseStatus()
        }
      }
    
      println("Another pattern matching without tupled")
      timer {
        infos.map {
          case (name, status) => Person(name, status).verboseStatus()
        }
      }
    
      println("Using unapply in a companion object that takes a tuple parameter")
      timer {
        infos.map { case PersonInfoU(p) => p.name + " is " + p.status }
      }
    }
    
    /*Results
      Using two map operations
      Time to perform code block 208ms
    
      Pattern matching and calling tupled
      Time to perform code block 130ms
    
      Another pattern matching without tupled
      Time to perform code block 130ms
    
      WINNER
      Using unapply in a companion object that takes a tuple parameter
      Time to perform code block 69ms
    */
    

    Assuming my test is sound, it seems the unapply in a companion-ish object was ~2x faster than the pattern matching, and pattern matching another ~1.5x faster than two maps. Each implementation probably has its use cases/limitations.

    I'd appreciate if anyone sees anything glaringly dumb in my testing strategy to let me know about it (and sorry about that var). Thanks!