This document discusses refactoring Scala code to improve quality and maintainability. It provides examples of issues like stringly typed code, abuse of collections, and poor scoping. The presentation recommends addressing these by using case classes instead of tuples, parsing values instead of passing raw strings, giving intermediate steps meaningful names, and separating logic and data through traits or type classes. Well-scoped code with a small public API and private implementation details is emphasized.
2. Agenda
• For the next 40
minutes, we’ll:
– Look at examples
– Discuss patterns
– … and anti-patterns
– Showcase refactoring
techniques
3. Our Victim
• … is ScalaChess
– Provides a full domain
model for chess
– Good test coverage
– Long-lived
– High quality code
– MIT license
– Integrated in lichess.org
* ScalaChess is an open-source project by Thibault Duplessis
5. Naming Things
• New features complicate matters:
– Infix notation (a.k.a dot-free syntax)
case class Geo(lat: Double, long: Double) {
def distance(other: Geo): Double = // ...
}
val (center, location): Geo = // ...
if (centre.distance(location) <= radius)
Some(loc) else None
6. Naming Things
• New features complicate matters:
– Infix notation (a.k.a dot-free syntax)
case class Geo(lat: Double, long: Double) {
def distance(other: Geo): Double = // ...
}
val (center, location): Geo = // ...
if (centre distance location <= radius)
Some(loc) else None What language is this?
7. Naming Things
• New features complicate matters:
– Infix notation (a.k.a dot-free syntax)
case class Geo(lat: Double, long: Double) {
def distanceFrom(other: Geo): Double = //…
}
val (center, location): Geo = // ...
if (location distanceFrom center <= radius)
Some(loc) else None A little clearer now
8. Naming Things
• Oh, the humanity: Symbolic operators
case class Geo(lat: Double, long: Double) {
def <->(other: Geo): Double = // ...
}
val Geo(center, location) = // ...
if (loc <-> center <= radius)
Some(loc) else None
Why would you do that?!
9. Naming Things
• There are worse offenders. Take Dispatch:
import dispatch.Http
import Http._
val server = url("http://example.com")
val headers = Map("Accept" -> "application/json")
Http(server >>> System.out) // GET
Http(server <:< headers >>> System.out) // GET
Http(server << yourPostData >|) // POST
10. Naming Things
• There are worse offenders. Take scalaz:
def s[A](a: A) = a.success[List[String]]
val add3 = (x: Int) =>
(y: Int) =>
(z: Int) => x + y + z
val res = (7) <*> (s(8) <*> (s(9) ∘ add3))
assert(res == s(24))
13. Stringly Typed
“Used to describe an implementation
that needlessly relies on strings when
programmer & refactor friendly options
are available.”
-- Coding
Horror
14. Stringly Typed
• Examples:
– Passing dates as strings
– Carrying unparsed data around
– Using empty strings instead of Options
case class Person(name: String, created: String)
def resolveConflict(p1: Person, p2: Person): Person = {
val c1 = dateParser.parse(p1.created)
val c2 = dateParser.parse(p2.created)
if (c1 compareTo c2 > 0) p1 else p2
}
1. Parser needs to be well-known
2. Error handling all over the place
3. What’s with all the boilerplate?
15. Stringly Typed
• Examples:
– Passing dates as strings
– Carrying unparsed data around
– Using empty strings instead of Options
case class Person(name: String, location: String)
def nearest(to: Person, all: List[Person]): Person = {
val geo: Point = Point.parse(to.location)
all.minBy(p => geo.distanceTo(Point.parse(p.location)))
}
1. Inefficient (space/time)
2. Error handling all over the place
3. What’s with all the boilerplate?
16. Stringly Typed
• Examples:
– Passing dates as strings
– Carrying unparsed data around
– Using empty strings instead of Options
case class Person(name: String, location: Point)
def nearest(to: Person, all: List[Person]): Person =
all.minBy(p => to.location distanceTo p.location)1. Efficient (only parsed once)
2. Sane error handling
3. Zero boilerplate!
17. Stringly Typed
• Examples:
– Passing dates as strings
– Carrying unparsed data around
– Using empty strings instead of Options
case class Person(firstName: String, lastName: String)
def render(p: Person): String =
s"""
|<div id='first-name'>${p.firstName}</div>
|<div id='last-name'>${p.lastName}</div>
""".stripMargin
1. Nothing enforces emptiness check!
2. Scala has a great type for these :-)
19. Collective Abuse
• Scala has a massive
collection library
• Loads of built-ins too
– Case classes
– Functions and partials
– Tuples, tuples, tuples
• Fairly easy to abuse
20. Collective Abuse
• Common anti-patterns:
– Too many inline steps
– Tuple overload
val actors: List[(Int, String, Double)] = // ...
def bestActor(query: String) =
actors.filter(_._2 contains query)
.sortBy(-_._3)
.map(_._1)
.headOption
1. What does this even do?!
2. How does data flow here?
21. Collective Abuse
• Common anti-patterns:
– Too many inline steps
– Tuple overload
val actors: List[(Int, String, Double)] = // ...
def bestActor(query: String) = {
val matching = actors.filter(_._2 contains query)
val bestByScore = matching.sortBy(-_._3).headOption
bestByScore.map(_._1)
} Name intermediate steps!
22. Collective Abuse
• Common anti-patterns:
– Too many inline steps
– Tuple overload
val actors: List[(Int, String, Double)] = // ...
def bestActor(query: String) =
actors.filter(_._2 contains query)
.sortBy(-_._3)
.map(_._1)
.headOption
What’s with all these
underscores?
23. Collective Abuse
• Common anti-patterns:
– Too many inline steps
– Tuple overload
case class Actor(id: Int, name: String, score: Double)
def bestActor(query: String, actors: List[Actor]) =
actors.filter(_.name contains query)
.sortBy(-_.score)
.map(_.id)
.headOption
Scala classes are cheap.
Use them.