Sunday, August 16, 2009

Assignment and Parameter Objects

One of the principle design goals of Scala is to be "deep" not wide, which means the language attempts to have a small set of rules that can be applied in many different ways in different situations. Pattern matching is one of my favourite examples of this. Pattern matching is commonly seen in match { case... } statements but you will also see it frequently in exception handling, function declaration and, most important for this post, assignment.

Scala does not have multiple assignment like some languages. Instead it has tuples and matching. Tuples are a light-weight way of grouping data in a simple data object. (1,2.0,'c',"string", true). A simple example of a 5 element tuple. Tuples can be up to 22 elements long and can be homogenous or heterogenous. Using this for multiple assignement works something like:
  1. val (firstname, lastname) = ("Jesse","Eichar")

This is pattern matching.
  1. scala> ("Jesse","Eichar") match {
  2.      | case (firstname,lastname) => println(firstname,lastname)
  3.      | }
  4. (Jesse,Eichar)

Notice that in both cases you need the brackets around firstname, lastname. This instructs the compiler that you are matching against a Tuple.

Now the interesting use is with parameter objects. Tuples are a poor substitute for parameter objects because they do not have context. Changing:
  1. def myMethod( firstname:String, middlename:String, lastname:String) = {...}

to
  1. def myMethod( name:(String,String,String)) = {...}

Is not a good change because you loose context. What are the 3 strings? The information must go in the Javadocs. A better option:
  1. caseclass Name(first:String, middle:String, last:String)
  2. def myMethod( name:Name ) = {
  3.   val Name(first, middle, last) = name
  4.   // do something with first middle last
  5. }

The beauty is that you have an object that you can pass around easily. It is a case class therefore extracting the information is incredibly easy and unlike a tuple it has context and can have methods added easily.

Yes it is longer to write but if you need to reuse the data in several locations the trade off is well worth it in clarity.

Examples:
  1. // define name data object.
  2. // Notice toString is a lazy val.  This means value is calculated only once.
  3. scala>caseclass Name(first:String, middle:String, last:String) {
  4.      | override lazy val toString="%s, %s %s" format (last, first,middle)
  5.      | }
  6. defined class Name
  7. // toString formats name nicely.
  8. scala> Name("Jesse","Dale","Eichar")
  9. res1: Name = Eichar, Jesse Dale
  10. scala>def readName() = {
  11.      | //maybe load from a file or database
  12.      | Name("Jesse","Dale","Eichar") :: Name("Jody","","Garnett") :: Name("Andrea","","Antonello"):: Nil
  13.      | }
  14. readName: ()List[Name]
  15. scala>def firstLastName(name:Name) = {
  16.      |  // we are putting _ because we don't care about middle name
  17.      | val Name( first, _, last ) = name
  18.      | (first, last)
  19.      | }
  20. firstLastName: (Name)(String, String)
  21. // convert the list of Names to a list of tuples of first and last name
  22. scala> readName().map( firstLastName _ )
  23. res2: List[(String, String)] = List((Jesse,Eichar), (Jody,Garnett), (Andrea,Antonello))
  24. // print all first names starting with J
  25. scala>for( Name(first,_,_) <- readName; if (first.startsWith("J"))) println(first)
  26. Jesse
  27. Jody
  28. // print the first and middle parts of the first name in the list
  29. scala> readName() match {
  30.      | // the :: _ indicates that we are matching against a list but we don't care about the rest of the list
  31.      | case Name(f,m,_) ::  _ => println( f, m)
  32.      | case _ => println("Not a list of names")
  33.      | }
  34. (Jesse,Dale)

1 comment:

  1. Ah! The lightbulb just went off over using case classes for matching! So I just went and replaced some tuples that were bothering me in some of my code. Thanks!

    ReplyDelete