This is a summary of the guidelines presented by Martin Odersky during his talk Scala with Style.
Pick the simplest thing that does the job
Example:
jp.getRwaClasspath.filter(
_.getEntryKind == IClasspathEntry.CPE_SOURCE).
iterator.flatMap(entry =>
flatten(ResourcesPlugin.getWorkspace.
getRoot.findMember(entry.getPath)))
Could have written:
val sources = jp.getRwaClasspath.filter(_.getEntryKind == IClasspathEntry.CPE_SOURCE)
def workspaceRoot = ResourcesPlugin.getWorkspace.getRoot
def filesOfEntry(entry: Set[File]) = flatten(workspaceRoot.findMember(entry.getPath))
sources.iterator flatMap filesOfEntry
- There's a lot of value in meaningful names
- Easy to add them using inline
val
s anddef
s
- Use
val
s, notvar
s - Use recursions or combinators, not loops
- Use immutable collections
- Concentrate on transformations, not CRUD
- Sometimes, mutable gives better performance.
- Sometimes (but not that often!) it adds convenience.
Local state is less harmful than global state
Example:
val (totalPrice, totalDiscount) =
items.foldLeft((0.0, 0.0)) {
case ((tprice, tdiscount), item) =>
(tprice + item.price,
tdiscount + item.discount)
}
Could have written:
var totalPrice, totalDiscount = 0.0
for (item <- items) {
totalPrice += item.price
totalDiscount += item.discount
}
An object is mutable if its (functional) behaviour depedens on its history
The cleanest and most elegant solutions do not always come to mind at once. That's ok, so keep it going!
Examples:
items + 10 vs items.+(10)
xs map f vs xs.map(f)
xf flatMap vs xs.flatMap(fun)
fun filterNot .filterNot(isZero)
isZero groupBy keyFn .groupBy(keyFn)
- If the method name is symbolic, always use infix.
- For alphanumeric method names, one can use infix if there is only one alphanumeric operator in the expression:
mapping add filter
- But prefer ".method(...)" for chained operators:
mapping.add(filter).map(second).flatMap(third)
Examples:
xs map f vs xs *|> f
vector add mx vs vector + mx
(xs.foldLeft(z))(op) vs (z /: xs)(op)
UNDEFINED vs ???
Use symbolic only if:
- Meaning is understood by your target audience, or
- Operator is standard in application domain, or
- You would like to draw attention to the operator (symbolic usually sticks out more than alphabetic).
Example:
// Loop
var i = 0
while(i < limit && !qualifies(i)) i += 1
i
// Recursion
def recur(i: Int): Int = {
if (i >= limit || qualifies(i)) else recur(i + 1)
}
recur(0)
// Combinators
(0 until length).find(qualifies).getOrElse(length)
- Consider using combinators first.
- If this becomes too tedious, or efficiency is a big concern, fall back on tail-recursive functions.
- Loops can be used in simple cases, or when the computation inherently modifies state.
Example:
// Procedure
def printBar(bar: Baz) {
println(bar)
}
// "="
def printBar(bar: Bar): Unit = {
println(bar)
}
- Don't use procedure syntax
This has also become part of the official Style Guide.
Example:
// Private
private def isJava(sym: Symbol): Boolean = {
sym hasFlag JAVA
}
def outer(owner: Symbol) = {
if (symbols exists isJava) ...
...
}
// Nested
def outer(owner: Symbol) = {
def isJava(sym: Symbol): Boolean = {
sym hasFlag JAVA
}
if (symbols exists isJava) ...
...
}
- Prefer nesting if you can save on parameters.
- Prefer nesting for small functions, even nothing is captured.
- Don't nest many levels deep.
Example:
class Shape
case class Circle(center: Point, radius: Double) extends Shape
case class Rectangle(ll: Point, ur: Point) extends Shape
case class Point(x: Double, y: Double) extends Shape
// Pattern matching
def area(s: Shape): Double = s match {
case Circle(_, r) =>
math.pi * r * r
case Rectangle(ll, ur) =>
(ur.x - ll.x) * (ur.y - ll.y)
case Point(_, _) =>
0
}
// Dynamic Dispatch
class Shape {
def area: Double
}
case class Circle(center: Point, radius: Double) extends Shape {
def area = math.pi * radius * radius
}
case class Rectangle(ll: Point, ur: Point) extends Shape {
def area = (ur.x - ll.x) * (ur.y - ll.y)
}
case class Point(x: Double, y: Double) extends Shape {
def area = 0
}
- It depends whether your system should be extensible or not.
- If you foresee extensions with new data alternatives, choose dynamic dispatch
- If you foresee adding new methods later, choose pattern matching.
- If the system is complex and closed, also choose pattern matching.
👍