Sponsored Link •
|
Summary
Reducing accidental complexity in Scala
Advertisement
|
update: part II is now available
Weaving a thread throughout these numbers, one idea is pervasive; the defect rate seems unaffected by the choice of programming language. So 1 in every 10 lines of Scala will contain a defect, as will 1 in every 10 lines of assembly. This is important when considering how many lines of code that each of these languages need in order to implement the same feature.
Given that the rate of defect creation remains constant and techniques for detection are slowing down, it makes sense that defects should be tackled from the other direction; instead of increasing the rate of detection we can reduce the rate of creation. Specifically, by reducing the number of lines of code.
Modern languages have already made great progress in this area. For example, studies comparing Scala and Java quote that the same feature requires anywhere from 3x to 10x fewer lines in the Scala implementation. The extra lines needed in Java are just accidental complexity; as well as providing more places for defects to appear they create more noise. This is not good news for the developer who later has to come back and read and maintain the code and is exactly what we want to avoid... longer code with more bugs that's harder to maintain.
There are a number of features in Scala that help keep the daemon boilerplate under control. Many of these have already been documented elsewhere, but they include:
This:
List(2,3,4).foreach(println)
is shorter than this:
for(x <- List(2,3,4)) { println(x) }
Instead of:
Map<Integer, String> theMap = new HashMap<Integer, String>
a Scala developer can write:
val theMap = HashMap[Int, String]
There's no need to duplicate the <Integer, String> construct.
Scala code also tends to produce better quality errors.
So less is more, right? Well, sometimes... if you're reducing accidental complexity then less is definitely a good thing. But not always; many design patterns recognised as good practice also tend to increase the line count of a system.
For example:
lots of Good Things(tm) will add code...
Of course, this isn't without benefit. Many patterns help to express intent more effectively and make the code more testable. Good refactoring and splitting up large blocks of functionality will make the code easier to maintain, and the newly-named fragements also help to document the code.
It might seem a contradiction that less lines are good and more lines are good, but the increased line count here isn't just adding accidental complexity, it's adding structure and intent and documentation. This all helps maintainers and testers to keep defects down, so the main goal is still being achieved!
With one exception... The use of forwarders in object composition, decorators, etc.
Take the following example (adapted from an article in Wikipedia):
abstract class I { def foo() def bar() def baz() } class A extends I { def foo() = println("a.foo") def bar() = println("a.bar") def baz() = println("a.baz") } class B(a : A) extends I { def foo() = a.foo() // call foo() on the a-instance def bar() = println("b.bar") def baz() = a.baz() } val a = new A val b = new B(a)
Here, class B implements the contract of I by delegating some of the work to an instance of A
In a worst case scenario this could lead to class B containing tens of forwarder methods that do nothing but call through an instance of A, with hundreds of lines of code just to state that:
For any functionality not implemented in this class, delegate to the member variable "a"
it's almost as bad as javabean properties...
If "A" doesn't need any additional logic to create an instance, and it's always constructed alongside an instance of B (or some other class) for purposes of delegation, then it can be made a trait - and the problem is solved:
trait A { def foo() = println("a.foo") def bar() = println("a.bar") def baz() = println("a.baz") } class B { def bar() = println("b.bar") } val b = new B with A
The trait A contains both the contract and default implementation for the methods (although it could also leave some definitions abstract if desired) Multiple traits can be mixed-in like this when constructing the value "b", which as shown is of type "B with A"
Mix-ins help, a lot! But if "A" has to be looked up via JNDI, or needs a factory method to construct, or already exists at the time we need to use it, then mix-ins are powerless to help.
Autoproxy is a Scala compiler-plugin created to help with exactly this situation
By using a simple annotation, the compiler can be instructed to generate delegates in situations where mix-ins just don't help.
Returning to the original example:
abstract class I { def foo() def bar() def baz() } class A extends I { def foo() = println("a.foo") def bar() = println("a.bar") def baz() = println("a.baz") } class B(@proxy a : A) extends I { def bar() = println("b.bar") } val a = new A val b = new B(a)
The @proxy annotation will generate the foo() and baz() methods in class B, identical to the hand-written versions shown previously.
Using @proxy with a trait, things become even easier:
trait A { def foo() = println("a.foo") def bar() = println("a.bar") def baz() = println("a.baz") } class B(@proxy a : A) { def bar() = println("b.bar") } val a = new A val b = new B(a)
Behind the scenes, traits are implemented as interfaces plus a separate class containing any concrete implementation. This means that @proxy can add A (the interface) to superclasses of B, allowing B to be used as an instance of A. There is no need to explicitly break out I as an inteface.
The wiki and source for the autoproxy plugin can be found on github.
In the next article I'll cover a few usage scenarios for the plugin
And after that, some of the challenges involved in adding this to the scala compiler, a process that one commentator described as "bear wrestling"
Have an opinion? Readers have already posted 18 comments about this weblog entry. Why not add yours?
If you'd like to be notified whenever Kevin Wright adds a new entry to his weblog, subscribe to his RSS feed.
Kevin Wright has finally settled back in London to work on market analysis for the telecoms industry after having worked his way around Europe in manufacturing, finance and even online gaming. He's a self-appointed Scala Evangelist and an active participant in every forum he can find, where he's currently trying to build interest in the London Scala Users' Group. |
Sponsored Links
|