Skip to content

Instantly share code, notes, and snippets.

@lregnier
Last active March 16, 2023 21:41
Show Gist options
  • Save lregnier/57179e016fad0028964bcfd9951e33cc to your computer and use it in GitHub Desktop.
Save lregnier/57179e016fad0028964bcfd9951e33cc to your computer and use it in GitHub Desktop.
Scala with Style, by Martin Odersky

Scala with Style (by Martin Odersky)

This is a summary of the guidelines presented by Martin Odersky during his talk Scala with Style.

Table of Contents

Guidelines

1. Keep it simple

Pick the simplest thing that does the job

2. Don't pack too much in one expression

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

Find meaningful names:

  • There's a lot of value in meaningful names
  • Easy to add them using inline vals and defs

3. Prefer Functional

By default:

  • Use vals, not vars
  • Use recursions or combinators, not loops
  • Use immutable collections
  • Concentrate on transformations, not CRUD

When to deviate:

  • Sometimes, mutable gives better performance.
  • Sometimes (but not that often!) it adds convenience.

4. But don't diabolize local state

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
}

5. Careful with mutable objects

An object is mutable if its (functional) behaviour depedens on its history

6. Don't stop improving too early

The cleanest and most elegant solutions do not always come to mind at once. That's ok, so keep it going!

Choices

1. Infix vs "."

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)

Recommendations

  1. If the method name is symbolic, always use infix.
  2. For alphanumeric method names, one can use infix if there is only one alphanumeric operator in the expression: mapping add filter
  3. But prefer ".method(...)" for chained operators: mapping.add(filter).map(second).flatMap(third)

2. Alphabetic vs Symbolic

Examples:

xs map f               vs    xs *|> f
vector add mx          vs    vector + mx
(xs.foldLeft(z))(op)   vs    (z /: xs)(op)
UNDEFINED              vs    ???

Recommendations

Use symbolic only if:

  1. Meaning is understood by your target audience, or
  2. Operator is standard in application domain, or
  3. You would like to draw attention to the operator (symbolic usually sticks out more than alphabetic).

3. Loop vs recursion vs combinators

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)

Recommendations

  1. Consider using combinators first.
  2. If this becomes too tedious, or efficiency is a big concern, fall back on tail-recursive functions.
  3. Loops can be used in simple cases, or when the computation inherently modifies state.

4. Procedure vs "="

Example:

// Procedure
def printBar(bar: Baz) {
  println(bar)
}

// "="
def printBar(bar: Bar): Unit = {
  println(bar)
}

Recommendations

  1. Don't use procedure syntax

This has also become part of the official Style Guide.

5. Private vs nested

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) ...
  ...
}

Recommendations

  1. Prefer nesting if you can save on parameters.
  2. Prefer nesting for small functions, even nothing is captured.
  3. Don't nest many levels deep.

6. Pattern matching vs dynamic dispatch

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
}

Recommendations

  1. It depends whether your system should be extensible or not.
  2. If you foresee extensions with new data alternatives, choose dynamic dispatch
  3. If you foresee adding new methods later, choose pattern matching.
  4. If the system is complex and closed, also choose pattern matching.
@MarcinSwierczynski
Copy link

MarcinSwierczynski commented Jun 12, 2018

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment