Post on 08-Jan-2017
Scala Refactorin
gTomer Gabel, CodeMotion Tel-Aviv 2015
for Fun and Profit
Agenda• For the next 40
minutes, we’ll:– Look at examples
– Discuss patterns
– … and anti-patterns– Showcase refactoring
techniques
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
STARTING OFF LIGHT
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
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?
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
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?!
Naming Things• There are worse offenders. Take Dispatch:
import dispatch.Httpimport Http._
val server = url("http://example.com") val headers = Map("Accept" -> "application/json")
Http(server >>> System.out) // GETHttp(server <:< headers >>> System.out) // GETHttp(server << yourPostData >|) // POST
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))
REAL-WORLD EXAMPLE TIME!
THE LAY OF THE LAND
Stringly Typed“Used to describe an implementation that needlessly relies on strings when programmer & refactor friendly options are available.” -- Coding Horror
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?
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
place3. What’s with all the
boilerplate?
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 handling3. Zero boilerplate!
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 :-)
REAL-WORLD EXAMPLE TIME!
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
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?
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!
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?
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.
REAL-WORLD EXAMPLE TIME!
Scoping
• Correct scoping is incredibly important!–Separation of concerns–Code navigation and discovery–Reduced autocompletion noise
Scoping• Scala offers an
impressive toolbox:– Nested scopes– Companion objects– Traits– Visibility modifiers– Type classes
Scopingclass User(var name: String, var age: Int, initialStatus: UserStatus = New) {
private var _status = initialStatus def status = _status
def delete(): Unit = { _status = Deleted Mailer.notify(Mailer.userDeletion, this) }}
Mutable data ⇒ Boilerplate
SoC broken!
EXAMPLE TIME!https://github.com/holograph/scala-refactoring
Scoping
• Lessons learned:–Keep logic and data separate
–Case classes are your friends!–Add “aspects” via type classes
Scoping
• Rules of the road:–Keep your public API small–Make everything else private–Put helpers in companions–… or via implicit extensions
Questions?
tomer@tomergabel.com@tomerghttp://il.linkedin.com/in/tomergabel
WE’RE DONE HERE!