There's been a lot of chatter lately about static vs. dynamic languages, code size, code density, etc. Many people lump Scala into the statically typed bucket because Scala has static typing. But what most folks don't realize is that Scala has most of the flexibility and syntactic economy of languages like Ruby and Python while supporting compiler checking of types to flag errors.
You may be confused. Over the next few postings, I'm going to demonstrate that most of the safe (taking input off the wire and doing method lookup on that input is not safe) constructs that dynamically typed languages have are available in Scala.
But first, you have to read the following:
- If you think I'm a crappy programmer and want to make posts about my stupidity and your superiority, go someplace else. But before you do, take a look in the lift repository. Most of that code is mine. I'm not the best programmer I've ever met, but I'm not stupid or some variant.
- I've been coding professionally for nearly 30 years. I've used virtually every popular language for commercial projects (except Lisp.) I've done nearly 2 years of Ruby. I've done 11 years of Java. I built the first real-time spreadsheet (Mesa) in a combination of Objective-C and C++. I understand, very, very intimately the difference between statically typed and dynamically typed languages. This isn't to say I can't learn more, but for those who have 2 years of Ruby under their belt after a couple of 200 level Java classes in college, don't bother flaming me.
So, one of the places that dynamic languages shine is the ability to pass an object (or a collection of objects) to a method and as long as the objects implement particular methods (e.g., name and age), the method will execute just fine. For example, a method that returns a string formatted with the name and age of each of the objects in the collection. This is particularly useful for functions that format instances of classes that have similar method naming, but not a similar base class.
Just like Ruby and Python, Scala can do this. The only thing that you have to do is declare the methods that the objects must implement as the "type" of the incoming parameters. Think of this extra "typing" as merely good documentation.
First, let's define a couple of classes:
case class Pet(name: String, age: Int, price: Int)
case class Antique(name: String, year: Int, cost: Int) {
def age = 2008 - year
}
In Scala,
case class creates a class that can be economically instantiated (no need for 'new') and has accessors for its parameters as well as deep comparators, toString, and hashCode methods.Next, let's create some collections of Pets and Antiques:
val pets = Pet("Elwood", 10, 25) :: Pet("Archer", 4, 600) :: Nil
val stuff = Antique("Couch", 1873, 8000) :: Antique("Vase", 1749, 2000) :: Nil
And let's define a method that takes a collection of anything with name and age methods and returns a nicely formatted string:
def nameAndAge(in: Seq[{def name: String; def age: Int}]) =
in.map(in => in.name+" is "+in.age+" years old").mkString("\n")
And let's call the methods:
println("Pets: "+Ext.nameAndAge(pets))
println("Stuff: "+Ext.nameAndAge(stuff))
Cool. It works like Ruby and Python. Yeah, yeah, you have to write a little extra stuff describing the methods used. So what? You want to do this anyway because it's part of documenting your code. The 20 or so characters are characters that wind up in the HTML documentation of the method. And, you get some free tests for this method because if you try to call it with parameters that don't match, the compiler flags the problem for you. You save some typing because you can write fewer tests.
But it gets better with Scala. If you don't have a uniform naming convention (for example, Pet has a price property and Antique has a cost property.) So in Ruby and Python, you can't use the same method to format the name and price/cost of both Pet and Antique. In Scala, there are two ways to do this using some excellent mojo called "view bounds".
You can define a lightweight class that contains just name and price: NameAndPrice. Next, you define a method that takes a list of things that can be converted into a NameAndPrice (that's what the <% means.)
case class NameAndPrice(name: String, price: Int)
def nameAndPrice[T <% NameAndPrice](in: Seq[T]) =
in.map(in => in.name+" price $"+in.price).mkString("\n")
The next thing to do is define a method that converts Pet or Antique into a NameAndPrice. You only need to define this method once and the method will be applied to all cases where you need to convert from Pet or Antique to NameAndPrice:
implicit def petToNaP(in: Pet) = NameAndPrice(in.name, in.price)
implicit def stuffToNaP(in: Antique) = NameAndPrice(in.name, in.cost)
Once again, our code compiles and runs correctly:
println("Pets (view): "+nameAndPrice(pets))
println("Stuff (view): "+nameAndPrice(stuff))
But it gets even better. We can combine these two mechanisms to allow arbitrary classes to be passed to a method and do implicit conversion if the class doesn't have the cost method but has a price method. First, let's look at a variant of the original example:
def cost[T <% {def cost: Int}](in: T) = in.cost
println("Stuff (ext & view): "+
stuff.map(i => i.name+" cost $"+cost(i)).mkString("\n"))
Next, let's convert something with a "price" method into something with a "cost" method:
case class CostHolder(cost: Int)
implicit def priceToCost(in: {def price: Int}) = CostHolder(in.price)
And the code compiles and does the right thing:
println("Pets (ext & view): "+pets.map(i => i.name+" cost $"+cost(i)).mkString("\n"))
So, Scala can do everything that Ruby and Python can do in terms of taking objects or collections of objects and "dynamically calling methods on them. But Scala does them one better with the ability to normalize method names across classes so you can use the same methods for classes with different naming conventions.
There are a couple of "points" that folks may make about this code:
- Those "implicit" things look really dangerous. They can be, but they are also scoped so one "invites" the implicit conversions into a module with an import statement. They are far less dangerous than AOP or Ruby's meta-programming/open classes, yet they serve the same purpose.
- There must be some performance penalty for not using classes, but just calling things by method name. Yes, there is, but the JVM HotSpot compiler mitigates this issue. The cost of one of these method calls is about 5x the cost of a regular method call... or about the same as the cost of a Ruby method call vs. a Java method call. The nice thing is you only have to use them when you need them, so the performance hit is localized rather than a global problem with all your code.
- All this creation of objects much cost a lot garbage collection-wise. Actually, no. The JVM's generational garbage collector is optimized for situations like this and because the implicitly created objects are short-lived, they'll GC very nicely.
That's it for the first installment. In the next installment, I'll show you have to dynamically change an object dispatch table at run-time so you can create your own proto-type style classes and then change the class behavior at runtime.