scalafmt

Opinionated code formatter for Scala

- Ólafur Geirsson, @olafurpg

Agenda

  • Why?
  • Demo.
  • Scalariform.
  • How it works.

WHY?

Reason 1: We have better things to do

Reason 2: Refactoring


// Before
UserRepo hasEmail me.id flatMap { user =>
  AccountRepo hasStatement user.id flatMap { statement =>
    BalanceRepo hasBalance statement.id
  }
}
// After
for {
  user <- UserRepo.hasEmail(me.id)
  statement <- AccountRepo.hasStatement(user.id)
  balance <- BalanceRepo.hasBalance(statement.id)
} yield balance

Wright et al., “Large-Scale Automated Refactoring Using ClangMR.”

Reason 3: It's tedious


// Columns 80                                                                  |
case class Split(modification: Modification,
                 cost: Int,
                 ignoreIf: Boolean = false,
                 indents: Vector[Indent[Length]],
                 policy: Policy = NoPolicy,
                 penalty: Boolean = false,
                 optimalAt: Option[OptimalToken] = None)

// Columns 80                                                                  |
case class Split(modification: Modification,
                 cost: Int,
                 ignoreIf: Boolean = false,
                 indents: Vector[Indent[Length]] = Vector.empty[Indent[Length]],
                 policy: Policy = NoPolicy,
                 penalty: Boolean = false,
                 optimalAt: Option[OptimalToken] = None)(implicit val line: sourcecode.Line)

// Columns 80                                                                  |
case class Split(
    modification: Modification,
    cost: Int,
    ignoreIf: Boolean = false,
    indents: Vector[Indent[Length]] = Vector.empty[Indent[Length]],
    policy: Policy = NoPolicy,
    penalty: Boolean = false,
    optimalAt: Option[OptimalToken] = None)(implicit val line: sourcecode.Line)

Reason 4: Coding styles are hard

"Any style guide written in English is either so brief that it’s ambiguous, or so long that no one reads it."

-- Bob Nystrom, Hardest Program I've Ever Written , Dart, Google.

Demo

Case-study 1: typelevel/cats

olafurpg/cats/pull/1

Scala files295
Lines of code17.493
Time to format23s
Diff+2,672 −2,372
Longest line136 chars

private[data] trait XorTMonadCombine[F[_], L] extends MonadCombine[XorT[F, L, ?]] with XorTMonadFilter[F, L] with XorTSemigroupK[F, L] {
-

scalafmt command:


    scalafmt --maxColumn 100 --continuationIndentCallSite 2 --javaDocs --files . -i

Before


-  implicit def constSemigroup[A: Semigroup, B]: Semigroup[Const[A, B]] = new Semigroup[Const[A, B]] {
-    ...
-  }

After


+  implicit def constSemigroup[A : Semigroup, B]: Semigroup[Const[A, B]] =
+    new Semigroup[Const[A, B]] {
+      ...
+    }

Before


-    Arbitrary(Gen.oneOf(
-      getArbitrary[A].map(Eval.now(_)),
-      getArbitrary[A].map(Eval.later(_)),
-      getArbitrary[A].map(Eval.always(_))))

After


+    Arbitrary(
+      Gen.oneOf(getArbitrary[A].map(Eval.now(_)),
+                getArbitrary[A].map(Eval.later(_)),
+                getArbitrary[A].map(Eval.always(_))))

Before


-   /**
-     * Lift a function into the context of an Arrow
-     */

After


+   /**
+    * Lift a function into the context of an Arrow
+    */

Before


-  def traverse[A: Arbitrary, B: Arbitrary, C: Arbitrary, M: Arbitrary, X[_]: Applicative, Y[_]: Applicative](implicit
-    ArbFA: Arbitrary[F[A]],
-    ArbXB: Arbitrary[X[B]],
-    ArbYB: Arbitrary[Y[B]],
-    ArbYC: Arbitrary[Y[C]],
-    M: Monoid[M],
-    ...

After


+  def traverse[A : Arbitrary,
+               B : Arbitrary,
+               C : Arbitrary,
+               M : Arbitrary,
+               X[_]: Applicative,
+               Y[_]: Applicative](implicit ArbFA: Arbitrary[F[A]],
+                                  ArbXB: Arbitrary[X[B]],
+                                  ArbYB: Arbitrary[Y[B]],
+                                  ArbYC: Arbitrary[Y[C]],
+                                  M: Monoid[M],
+                                  ...

Before


trait AllInstances
-  extends FunctionInstances
-  with    StringInstances
-  with    EitherInstances
-  with    ListInstances
-  with    OptionInstances
-  with    SetInstances
-  with    StreamInstances

After


+    extends FunctionInstances with StringInstances with EitherInstances with ListInstances
+    with OptionInstances with SetInstances with StreamInstances

Case-study 2: lichess.org

olafurpg/lila/pull/1

Scala files743
Lines of code63.228
Time to format51s
Diff+3,870 −3,174
Longest line147 chars

protected def FormFuResult[A, B: Writeable: ContentTypeOf](form: Form[A])(err: Form[A] => Fu[B])(op: A => Fu[Result])(implicit req: Request[_]) =
-

scalafmt command:


scalafmt --maxColumn 100 --continuationIndentCallSite 2 --style defaultWithAlign --files . -i
//

Before


-  protected def SocketOptionLimited[A: FrameFormatter](consumer: TokenBucket.Consumer, name: String)(f: Context => Fu[Option[(Iteratee[A, _], Enumerator[A])]]) =
-

After


+  protected def SocketOptionLimited[A : FrameFormatter](
+      consumer: TokenBucket.Consumer, name: String)(
+      f: Context => Fu[Option[(Iteratee[A, _], Enumerator[A])]]) =

Before


-      WS.url(url).get().map(_.body).mon(_.security.proxy.request.time).flatMap { str =>
-        ...
-      }.addEffects( ... )

After


+      WS.url(url)
+        .get()
+        .map(_.body)
+        .mon(_.security.proxy.request.time)
+        .flatMap { str =>
+          ...
+        }
+        .addEffects( ... )

Before


-      case (((((((((((((nbUsers, ranks), nbPlaying), nbImported), crosstable), ratingChart), nbFollowing), nbFollowers), nbBlockers), nbPosts), isDonor), trophies), insightVisible), playTime) =>
-

After


+      case (((((((((((((nbUsers, ranks), nbPlaying), nbImported), crosstable),
+                    ratingChart),
+                   nbFollowing),
+                  nbFollowers),
+                 nbBlockers),
+                nbPosts),
+               isDonor),
+              trophies),
+             insightVisible),
+            playTime) =>

Before


-        Ok.chunked(Enumerator.outputStream(env.pngExport(game))).withHeaders(
-          CONTENT_TYPE -> "image/png",
-          CACHE_CONTROL -> "max-age=7200")

After


+        Ok.chunked(Enumerator.outputStream(env.pngExport(game)))
+          .withHeaders(CONTENT_TYPE  -> "image/png",
+                       CACHE_CONTROL -> "max-age=7200")

+Ok(
+    Json.obj(
+        "api"                                    -> Json.obj("current" -> api.currentVersion,
+                          "olds"                 -> api.oldVersions.map { old =>
+                        Json.obj("version"       -> old.version,
+                                 "deprecatedAt"  -> old.deprecatedAt,
+                                 "unsupportedAt" -> old.unsupportedAt)
+                      })
+    )) as JSON

What it should look like


+    Ok(
+        Json.obj(
+            "api" -> Json.obj("current" -> api.currentVersion,
+                              "olds"    -> api.oldVersions.map { old =>
+                            Json.obj("version"       -> old.version,
+                                     "deprecatedAt"  -> old.deprecatedAt,
+                                     "unsupportedAt" -> old.unsupportedAt)
+                     })
+        )) as JSON

Try it yourself

  • SBT plugin
  • IntelliJ plugin
  • brew install olafurpg/scalafmt/scalafmt
  • Download scalafmt.jar via Github releases.
  • See documentation.

Where are we now?

  • Can format almost any Scala code.
  • Formatting options:
    • --style default,
    • --style defaultWithAlign,
    • --style scalaJs (experimental)
    • --maxColumn 120
    • --javaDocs / --scalaDocs
    • --continuationIndentCallSite 2

Scalariform?

--maxColumn

How does scalafmt work?

scala.meta

Tokenizer: String => scala.meta.Tokens


scala> import scala.meta._
scala> """object Main extends App { world =>
            println(s"Hello $world!")
          }
       """.tokenize.get
       res3: Tokens = Tokens(
         BOF (0..0),
         object (0..6),
           (6..7),
         Main (7..11),
           (11..12),
         extends (12..19),
           (19..20),
         App (20..23),
           (23..24),
         { (24..25),
           (25..26),
         world (26..31),
       ...

scala.meta

Parser: String => scala.meta.Tree


scala> import scala.meta._
scala> """object Main extends App { world =>
            println(s"Hello $world!")
          }
          """.parse[Stat].get.show[Structure]
res9: String = """
Defn.Object(Nil, Term.Name("Main"),
            Template(Nil, Seq(Ctor.Ref.Name("App")),
            Term.Param(Nil, Term.Name("world"), None, None),
            Some(Seq(Term.Apply(Term.Name("println"),
                                Seq(Term.Interpolate(Term.Name("s"),
                                                     Seq(Lit("Hello "),
                                                     Lit("!")),
                                                     Seq(Term.Name("world")))))))))
"""

TeX: wrapping text

Every line break has a penalty


// 50 columns                                    |
object BestFirstSearch {                                        // 1 penalty
  DBObject(User(Name("Martin", "Odersky"),                      // 5 penalty
                Language("Scala")),                             // 3 penalty
           Address("Lausanne", "Switzerland"))                  // 0 penalty
}                                                               // 0 penalty
                                                                //----------
                                                                // 9 total

Exceeding column limit is expensive


// 50 columns                                    |
object BestFirstSearch {                                       // 1    penalty
  DBObject(User(Name("Martin", "Odersky"), Language("Scala")), // 1002 penalty
           Address("Lausanne", "Switzerland"))                 // 0    penalty
}                                                              // 0    penalty
                                                               //-------------
                                                               // 1002 total

Try all the combinations using best-first search


// 50 columns                                    |
object BestFirstSearch {
  DBObject(User(Name("Martin", "Odersky"), Language("Scala")), Address("Lausanne", "Switzerland"))

  DBObject(
      User(Name("Martin", "Odersky"), Language("Scala")), Address("Lausanne", "Switzerland"))

  DBObject(User(Name("Martin", "Odersky"), Language("Scala")),
           Address("Lausanne", "Switzerland"))

  DBObject(
      User(Name("Martin", "Odersky"), Language("Scala")),
      Address("Lausanne", "Switzerland"))

  DBObject(User(Name("Martin", "Odersky"),
                Language("Scala")),
           Address("Lausanne", "Switzerland"))

}

Testing?

Property 1: AST preservation

ast(code) == ast(format(code))

Property 2: Idempotency

format(code) == format(format(code))

See #192.

More important problems, vertical alignment


object VerticalAlignment {
  def name   = column[String]("name")
  def status = column[Int]("status")

  for {
    dao  <- olafur   \/> "Can't find olafur"
    user <- dao.user \/> "Join failed: no user object"
  }

  libraryDependencies ++= Seq(
    "org.scalameta" %  "scalameta"  % "0.1.0-RC4-M10",
    "com.lihaoyi"   %% "sourcecode" % "0.1.1"
  )
}

Summary.

  • Code formatting has many benefits.
  • Coding styles are hard.
  • scalafmt is out there.
  • scalafmt is still very young

Roadmap

  • Bugs, please report weird formatting output!
  • More coding styles: spark, typelevel, ...
  • Coding style detection.
  • Format git diff.
  • Format docstrings.
  • Incremental formatting.
  • scala-tidy
  • Dynamic style configuration.
  • ...

THANK YOU

- Visit documentation: scalafmt.org
- See slides: geirsson.com/assets/flatmap
- Contribute with PRs and reporting issues.
- Follow @olafurpg on Twitter.
- Chat on Gitter.