Wednesday, November 30, 2011

Learning Scala : "case class", twitter interview question?

Authored by Win Myo Htet


I have been learning Scala actively and reading blogs, stackoverflow, and books; browsing repo, getting my hands dirty coding snippet and writing blog. I come to this subject "case class". Well, I have to admit that reading the books is boring unlike blog, yet, the books are more reliable sources of information (oh yeah? You don't know that ?) Then, there is stackoverflow, which is more reliable, does not detour and gives instant enlightened answers/insights proof-read by professionals. Still, the book does a better job of explaining (better experience on ebook with the help of Glossary and Index) Yes, I will talk about the "case class". I digress in introduction!

As I crawl the internet to learn Scala, I have downloaded some twitter's open source popular repos on github. I have browsed, skimmed and searched the repos to see how the early scala adopter is using it and defining the Scala usage pattern. Here are the downloaded repos(watchers) : flockdb (1,133), finagle(592), gizzard(966), killdeer(18) and ostrich(179). With only these opened in the eclipse, I use eclispe search tool for some of the thing I am interested in. Search keyword and number found paired.

tailrec 17
foldLeft 19
foldRight 0
for 2945 /*This includes "for" from code comment*/
while 152 /*This also includes "while" from code comment*/

Yup, at that time, I want to know how pervasive is the TCO'ed recursion and fold usages. I guess, for-comprehension rules the day. And I also skim through the codes and notice quite a lot of case block matchings. So here are some stats:

case 1658 /*This includes "just in case ..." kind of comment*/
case class 187 /*don't know if this words pair can be in comment*/

Now I hope to have your attention on the seriousness of the subject matter. It will be on the twitter interview questions! Ok, how is "case class" different from "class"? What is "case class"? "case class" is just a class. Ok... It adds extra niceties-methods : copy, equal, hashcode and (prettier) toString. The important part is that when "case class" is used, the compiler creates companion object which has both apply and unapply methods. "apply(variables)" method in Scala can be replaced with just "(variables)", thus "case class" enables constructor without keyword "new".

scala> case class MyCaseClass(name:String,isCase:Boolean)
defined class MyCaseClass

scala> val myCaseClass=MyCaseClass("myCaseClass",true)
myCaseClass: MyCaseClass = MyCaseClass(myCaseClass,true)

scala> class MyClass(name:String,isCase:Boolean)
defined class MyClass

scala> val myClass=MyClass("myClass",false)
<console>:7: error: not found: value MyClass
       val myClass=MyClass("myClass",false)
                   ^

scala> val myClass=new MyClass("myClass",false)
myClass: MyClass = MyClass@be7f971

So the interviewer asks "Why is that "case class" constructor does not require "new" keyword while the regular one does?" Then, you will answer "Cause the compiler generate "companion object" with "apply" method which can be omitted" Then, the interviewer will ask "We don't care much about "new" keyword but we just want to use regular class and want the benefit of case pattern matching. How would you do that?" Don't worry. Here is the answer for you. "case class" pattern matching is made possible by companion object's unapply extractor method. So we just need to do what "case class" does!

Welcome to Scala version 2.9.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_26).
Type in expressions to have them evaluated.
Type :help for more information.

scala> case class MyCaseClass(name:String,isCase:Boolean)
defined class MyCaseClass

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

class MyClass(val name:String,val isCase:Boolean)
object MyClass {
  def unapply(myClass:MyClass):Option[(String,Boolean)]={
    if(myClass eq null) None
    Some((myClass.name,myClass.isCase))
  }
}

// Exiting paste mode, now interpreting.

defined class MyClass
defined module MyClass

scala> val myCaseClass=MyCaseClass("myCaseClass",true)
myCaseClass: MyCaseClass = MyCaseClass(myCaseClass,true)

scala> val myClass=new MyClass("myClass",false)
myClass: MyClass = MyClass@7bc2f501

scala> val classList=List(myClass,myCaseClass)
classList: List[ScalaObject] = List(MyClass@7bc2f501, MyCaseClass(myCaseClass,true))

scala> for( x <- classList) x match{
     |   case MyCaseClass(name,isCase)=>println(x+" "+name+" "+isCase)
     |   case MyClass(name,isCase)=>println(x+" "+name+" "+isCase)
     | }
$line2.$read$$iw$$iw$MyClass@7bc2f501 myClass false
MyCaseClass(myCaseClass,true) myCaseClass true

scala> 


In case, if you miss it, I have started the new repl session and redefine the classes. You will notice the ":paste" before MyClass definition tho. It is because we want the repl session to know that the following object with unapply method is the companion object to the class above. If you forget ":paste", don't worry, the repl will warn you. "unapply" method takes MyClass object and return the fields like the default constructor wrapped in the Option. Then we see the unapply extractor method of MyClass at work for pattern matching in the for loop near the bottom like MyCaseClass. Do you notice how pretty "case class" makes MyCaseClass's toString ? Your interviewer will then said "Ok, I am convinced that "case class" is better. What else does it do? How about if I want a new constructor?" Well, "case class" makes default constructor fields to be immutable (Scala always encourages immutable type.), so you don't have to state "val" explicitly. However, you will have to do that for "var" tho. If you want new constructor, you can create them yourself but to use it, you have to use "new" keyword because there won't be any compiler generated overloaded "apply" method for that new constructor.

Then, the interviewer is happy and you get the job at twitter. Don't forget me when twitter goes IPO and you become rich,OK?

Update
So, one interviewee came back to me and said that the interviewer said there are more to the case class. Well, if it is said so, let's take a look:

aunndroid@ubuntu:/host/linux/learning_scala/notes$ cat MyCaseClass.scala
case class MyCaseClass(name:String,isCase:Boolean){
  override def productPrefix="MyPrettyCaseClass"
}


We will use scalac -print option to peep into it. Please do it at home, it is safe. Actually I have omitted some interesting lines of codes for brevity, that's why.

aunndroid@ubuntu:/host/linux/learning_scala/notes$ scalac -print MyCaseClass.scala 
[[syntax trees at end of cleanup]]// Scala source: MyCaseClass.scala
package <empty> {
  case class MyCaseClass extends java.lang.Object with ScalaObject with Product with Serializable {
  .
  .
  .

  };
  final <synthetic> object MyCaseClass extends scala.runtime.AbstractFunction2 with ScalaObject with Serializable {
  .
  .
  .
  
  }
}


Now, we know that case class also has mixin of Serializable and Product trait. We know SerializableWhat is Product? Product enables the case class' fields to be accessed without reflection. Here is the Product scaladoc API and the following is the code snippet.

aunndroid@ubuntu:/host/linux/learning_scala/notes$ scala
Welcome to Scala version 2.9.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_26).
Type in expressions to have them evaluated.
Type :help for more information.

scala> :load MyCaseClass.scala
Loading MyCaseClass.scala...
defined class MyCaseClass

scala> val myCaseClass=MyCaseClass("aunnDroidCaseClass",true)
myCaseClass: MyCaseClass = MyPrettyCaseClass(aunnDroidCaseClass,true)

scala> myCaseClass.productArity
res3: Int = 2

scala> myCaseClass.productElement(0)
res4: Any = aunnDroidCaseClass

scala> myCaseClass.productElement(1)
res5: Any = true

scala> for(x <- myCaseClass.productIterator)println(x)
aunnDroidCaseClass
true

scala> myCaseClass.productPrefix
res7: java.lang.String = MyPrettyCaseClass


We have skipped the canEqual function. productArity provides the number of fileds in the class while productElement (n: Int) let us access those field with index. productIterator gives us the Iterator[Any] object which we use with for comprehension to print out the fields. We have overriden productPrefix, which is part of the reason why the case class has a pretty toString method.

Hope the interviewer is happy this time.

Authored by Win Myo Htet

No comments:

Post a Comment