class: center, middle # Scala Best Practices I Wish Someone'd Told Me About Nicolas Rinaudo • [@NicolasRinaudo] • [Besedo] --- ## Overview This is meant for Scala developers of beginner to intermediate levels. The goal is to list common bad practices often seen in Scala code, and how to avoid them. Each slide will start with a common pattern. See if you can work out what's wrong with it! --- ## Array comparison .section[Common implementation] ```scala Array(1) == Array(1) ``` -- .section[Bad because] it does not work ```scala Array(1) == Array(1) // res1: Boolean = false ``` --- ## Array comparison .super[2] .section[Prefer] using `sameElements` ```scala Array(1).sameElements(Array(1)) // res2: Boolean = true ``` --- ## Array comparison .super[3] .section[Future improvements] 2.13 introduces [immutable arrays](https://scala-lang.org/files/archive/api/2.13.x/scala/collection/immutable/ArraySeq.html) which behave as expected Mutable arrays are still 🤪 though --- ## Type annotation .section[Common implementation] ```scala def asOption[A](a: A) = Some(a) ``` -- .section[Bad because] the return type is not what you'd expect ```scala def asOption[A](a: A) = Some(a) // asOption: [A](a: A)Some[A] ``` --- ## Type annotation .super[2] .section[Prefer] explicitly type-annotated public members ```scala def asOption[A](a: A): Option[A] = Some(a) // asOption: [A](a: A)Option[A] ``` -- .section[Future improvements] Scala 3 requires type annotations for implicits --- ## Unicode operators .section[Common implementation] ```scala 1 → 4 / 2 ``` -- .section[Bad because] not actually equivalent to the ASCII operator ```scala 1 → 4 / 2 //
:13: error: value / is not a member of (Int, Int) // 1 → 4 / 2 // ^ ``` --- ## Unicode operators .super[2] .section[Prefer] the ASCII operator ```scala 1 -> 4 / 2 // res2: (Int, Int) = (1,2) ``` -- .section[Future improvements] Deprecated in [2.13](https://github.com/scala/scala/pull/7540) --- ## Enumerations .section[Common implementation] ```scala object Status extends Enumeration { val Ok, Nok = Value } ``` --- ## Enumerations .super[2] .section[Bad because] This compiles without a non-exhaustivity warning ```scala def foo(w: Status.Value): Unit = w match { case Status.Ok => println("ok") } ``` -- And fails at runtime ```scala foo(Status.Nok) // scala.MatchError: Nok (of class scala.Enumeration$Val) // at .foo(
:13) // ... 43 elided ``` --- ## Enumerations .super[3] .section[Prefer] sealed trait hierarchies (ADTs) ```scala sealed trait Status object Status { case object Ok extends Status case object Nok extends Status } ``` --- ## Enumerations .super[4] The compiler now knows we're not exhaustive: ```scala def foo(w: Status): Unit = w match { case Status.Ok => println("ok") } //
:14: warning: match may not be exhaustive. // It would fail on the following input: Nok // def foo(w: Status): Unit = w match { // ^ // foo: (w: Status)Unit ``` --- ## Enumerations .super[5] .section[Future improvements] Scala 3 might eventually [deprecate](https://gitter.im/lampepfl/dotty?at=5ceaece6ecdf942b4c4de347) [`Enumeration`](https://www.scala-lang.org/api/current/scala/Enumeration.html) in favour of [enum](https://dotty.epfl.ch/docs/reference/enums/enums.html) --- ## Sealed traits .section[Common implementation] ```scala // File Foo.scala sealed trait Foo class Bar extends Foo ``` -- .section[Bad because] the seal might leak ```scala // File FooBar.scala class FooBar extends Bar ``` --- ## Sealed traits .super[2] .section[Prefer] `final` or `sealed` descendants ```scala sealed trait Foo final class Bar extends Foo ``` --- ## Algebraic Data Types .section[Common implementation] ```scala sealed trait Status object Status { case object Ok extends Status case object Nok extends Status } ``` -- .section[Bad because] noisy type inference ```scala List(Status.Ok, Status.Nok) // res0: List[Product with Serializable with Status] = List(Ok, Nok) ``` --- ## Algebraic Data Types .super[2] .section[Prefer] having the root type extend [`Product`](https://www.scala-lang.org/api/current/scala/Product.html) and [`Serializable`](https://www.scala-lang.org/api/current/scala/Serializable.html) ```scala sealed trait Status extends Product with Serializable object Status { case object Ok extends Status case object Nok extends Status } ``` -- The correct type is now inferred: ```scala List(Status.Ok, Status.Nok) // res1: List[Status] = List(Ok, Nok) ``` --- ## Algebraic Data Types .super[3] .section[Future improvements] Scala 3's `enum` does a better job of hiding the issue, but [doesn't fix it](https://contributors.scala-lang.org/t/could-adts-extend-product-with-serializable-in-dotty/3304/59) --- ## Case classes .section[Common implementation] ```scala case class Foo(i: Int) ``` -- .section[Bad because] descendants don't behave as you'd expect ```scala class Bar(i: Int, s: String) extends Foo(i) ``` ```scala new Bar(1, "foo") == new Bar(1, "bar") // res0: Boolean = true ``` --- ## Case classes .super[2] .section[Prefer] final case classes ```scala final case class Foo(i: Int) ``` --- ## Custom extractors .section[Common implementation] ```scala object ExtractSome { def unapply[A](s: Some[A]): Option[A] = s } ``` --- ## Custom extractors .super[2] .section[Bad because] this compiles without a non-exhaustivity warning ```scala def foo[A](o: Option[A]): A = o match { case ExtractSome(a) => a } ``` -- And fails at runtime ```scala foo(None) // scala.MatchError: None (of class scala.None$) // at .foo(
:13) // ... 43 elided ``` --- ## Custom extractors .super[3] .section[Prefer] returning type [`Some`](https://www.scala-lang.org/api/current/scala/Some.html) for irrefutable extractors ```scala object ExtractSome { def unapply[A](s: Some[A]): Some[A] = s } ``` --- ## Custom extractors .super[4] We're now getting the expected warning: ```scala def foo[A](o: Option[A]): A = o match { case ExtractSome(a) => a } //
:13: warning: match may not be exhaustive. // It would fail on the following input: None // def foo[A](o: Option[A]): A = o match { // ^ // foo: [A](o: Option[A])A ``` --- ## Custom extractors .super[5] .section[Future improvements] Scala 3's exhaustivity checker is [much improved](https://infoscience.epfl.ch/record/225497/files/p61-liu.pdf), fixing most of these oddities --- ## Structural types .section[Common implementation] ```scala def add1(x: { def get: Int }): Int = 1 + x.get final case class Wrapper(i: Int) { def get: Int = i } ``` ```scala add1(Wrapper(1)) // res0: Int = 2 ``` -- .section[Bad because] * runtime cost * availability on the JVM depends on [runtime configuration](https://docs.oracle.com/javase/7/docs/api/java/lang/SecurityManager.html) --- ## Structural types .super[2] .section[Prefer] type classes ```scala trait HasGet[A] { def get(a: A): Int } implicit val wrap: HasGet[Wrapper] = new HasGet[Wrapper] { def get(a: Wrapper) = a.i } def add1[A](a: A)(implicit hga: HasGet[A]): Int = hga.get(a) + 1 ``` ```scala add1(Wrapper(1)) // res1: Int = 2 ``` --- ## Structural types .super[3] .section[Future improvements] Scala 3's [structural types](https://dotty.epfl.ch/docs/reference/changed-features/structural-types.html) are far more flexible, but still default to reflection --- ## Exceptions .section[Common implementation] ```scala def parseInt(str: String): Int = str.toInt def elementAt[A](value: Array[A], i: Int): A = value(i) ``` -- .section[Bad because] throws exceptions, which is: * basically a `GOTO` statement * unchecked (the compiler cannot enforce code validity) * referentially opaque --- ## Exceptions .super[2] If `throw` was referentially transparent, `foo1` and `foo2` would be equivalent in: ```scala def foo1() = if(false) throw new Exception else 2 def foo2() = { val a = throw new Exception if (false) a else 2 } ``` --- ## Exceptions .super[3] `foo1` terminates: ```scala foo1() // res0: Int = 2 ``` -- `foo2` does not: ```scala foo2() // java.lang.Exception // at .foo2(
:13) // ... 43 elided ``` --- ## Exceptions .super[4] .section[Prefer] * [`Option`](http://www.scala-lang.org/api/2.12.2/scala/Option.html) to convey the potential absence of a value * [`Either`](http://www.scala-lang.org/api/2.12.2/scala/util/Either.html) to convey a potentially failing computation * [`Try`](http://www.scala-lang.org/api/2.12.2/scala/util/Try.html), if you have to deal with code that throws --- ## Errors .section[Common implementation] ```scala sealed trait DbError extends Product with Serializable object DbError { case object InvalidSql extends DbError case object ConnectionLost extends DbError } ``` -- .section[Bad because] existing APIs ([`Future`](https://www.scala-lang.org/api/current/scala/concurrent/Future.html), [`Try`](https://www.scala-lang.org/api/current/scala/util/Try.html)...) rely on [`Exception`](https://docs.oracle.com/javase/7/docs/api/java/lang/Exception.html) to convey errors --- ## Errors .super[2] .section[Prefer] extending [`Exception`](https://docs.oracle.com/javase/7/docs/api/java/lang/Exception.html) (but no throwing!) ```scala sealed trait DbError extends Exception with Product with Serializable object DbError { case object InvalidSql extends DbError case object ConnectionLost extends DbError } ``` --- ## Errors .super[3] We can now work with "clean" types: ```scala // Turn an Either into a Try when needed Left(DbError.InvalidSql).toTry // Fail a Future with a useful error scala.concurrent.Future.failed(DbError.InvalidSql) ``` ??? I'm not a big fan of using `Exception` as a universal super type for errors, which is why I don't insist on it in writing. Maybe worth bringing up, since Martin Odersky told me he though that was a desirable practice. --- ## The `return` keyword .section["Common" implementation] ```scala def add(n: Int, m: Int): Int = return n + m ``` -- .section[Bad because] * basically a `GOTO` statement * breaks referential transparency * it doesn't do what you think it does --- ## The `return` keyword .super[2] ```scala def foo(is: List[Int]): List[Int] = is.map(n => return n + 1) //
:13: error: type mismatch; // found : Int // required: List[Int] // is.map(n => return n + 1) // ^ ``` Doesn't seem to make sense, but let's change the type... --- ## The `return` keyword .super[3] ```scala def foo(is: List[Int]): Int = is.map(n => return n + 1) //
:13: error: type mismatch; // found : List[Nothing] // required: Int // is.map(n => return n + 1) // ^ ``` Fine, let's play along... --- ## The `return` keyword .super[4] ```scala def foo(is: List[Int]): List[Nothing] = is.map(n => return n + 1) //
:13: error: type mismatch; // found : Int // required: List[Nothing] // is.map(n => return n + 1) // ^ ``` -- **(╯°□°)╯︵ ┻━┻** --- ## The `return` keyword .super[5] .section[Prefer] never using `return` -- .section[Future improvements] possibly in [Scala 3](https://github.com/lampepfl/dotty/issues/4240) --- ## Implicit conversions .section[Common implementation] ```scala implicit def str2int(str: String): Int = Integer.parseInt(str) ``` -- .section[Bad because] we use types to prevent us from doing illegal operations ```scala "foobar" / 2 // java.lang.NumberFormatException: For input string: "foobar" // at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) // at java.lang.Integer.parseInt(Integer.java:580) // at java.lang.Integer.parseInt(Integer.java:615) // at .str2int(
:12) // ... 43 elided ``` --- ## Implicit conversions .super[2] .section[Prefer] explicit conversions ```scala scala.util.Try(Integer.parseInt("foobar")).map(_ / 2) // res1: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "foobar") ``` Note: implicit conversions to enrich existing types (type class "syntax") are ok. --- ## Implicit conversions .super[3] .section[Future improvements] * Scala 3 locks this behind a [compilation flag](https://dotty.epfl.ch/docs/reference/contextual/conversions.html) * [Extension methods](https://dotty.epfl.ch/docs/reference/contextual/extension-methods.html) have a separate mechanism in Scala 3 --- ## Implicit resolution .section[Common implementation] ```scala case class Config(port: Int) def getPort(implicit c: Config): Int = c.port object a1 { implicit val defaultConfig: Config = Config(8080) } ``` ```scala { import a1._ getPort } // res0: Int = 8080 ``` --- ## Implicit resolution .super[2] .section[Bad because] identical names clash ```scala object a2 { implicit val defaultConfig: Int = 12345 } ``` ```scala { import a1._, a2._ getPort } //
:19: error: could not find implicit value for parameter c: Config // getPort // ^ ``` --- ## Implicit resolution .super[3] Clear when you remove the implicits: ```scala { import a1._, a2._ defaultConfig } //
:18: error: reference to defaultConfig is ambiguous; // it is imported twice in the same scope by // import a2._ // and import a1._ // defaultConfig // ^ ``` --- ## Implicit resolution .super[4] .section[Prefer] * very explicit names, including types * unfortunately, no foolproof method I know of -- .section[Future improvements] * Scala 3's [`import implied`](https://dotty.epfl.ch/docs/reference/contextual/import-implied.html) make the situation less likely * Implicit name shadowing [isn't a thing](http://dotty.epfl.ch/docs/reference/changed-features/implicit-resolution.html) in Scala 3 anyway --- ## String concatenation .section[Common implementation] ```scala 1 + "2" // res0: String = 12 ``` -- .section[Bad because] `+` often does not behave coherently ```scala List("foo") + "bar" // res1: String = List(foo)bar Set("foo") + "bar" // res2: scala.collection.immutable.Set[String] = Set(foo, bar) ``` --- ## String concatenation .super[2] Let's not even bring this abomination up: ```scala List(1) + 2 //
:13: error: type mismatch; // found : Int(2) // required: String // List(1) + 2 // ^ ``` --- ## String concatenation .super[3] .section[Prefer] * explicit calls to `toString` * string interpolation ```scala val foo = "FOO" // foo: String = FOO s"${foo}bar" // res4: String = FOObar ``` --- ## String concatenation .super[4] .section[Future improvements] * deprecated in [2.13](https://twitter.com/eed3si9n/status/1126960037086339072) * removed from [2.14](https://twitter.com/eed3si9n/status/1126960825703514112) * mysteriously fully supported in [Scala 3](https://twitter.com/NicolasRinaudo/status/1126947756583661574) ??? Currently in a weird state: - deprecated in 2.13 - removed from 2.14 - fully supported in Scala 3 --- class: center, middle # Conclusions --- ## Enforcing best practices * [Code reviews](https://smartbear.com/SmartBear/media/pdfs/Best-Kept-Secrets-of-Peer-Code-Review_Redirected.pdf) * Automated tools * [scapegoat](https://github.com/sksamuel/scapegoat) * [wartremover](http://www.wartremover.org) * [scala linter](https://github.com/HairyFotr/linter) * [scalastyle](http://www.scalastyle.org) * `scalac`, with sane options (see [Rob Norris' post on the subject](http://tpolecat.github.io/2017/04/25/scalac-flags.html)) * [scalafix](https://github.com/scalacenter/scalafix) --- ## More good and bad practices These were my favourite examples. I'm sure there are others. I'd be happy to take suggestions for additions to this list, either now or after the talk! --- ## Special thanks The [`scala/contributors`](https://gitter.im/scala/contributors) and [`lampepfl/dotty`](https://gitter.im/lampepfl/dotty) Gitter channels, and especially [Guillaume Martres](https://github.com/smarter). They basically wrote the _Future improvements_ sections. --- ## More information Slides available on https://nrinaudo.github.io/talk-scala-best-practices/ Articles on these are available on https://nrinaudo.github.io/scala-best-practices/ Find me on Twitter ([@NicolasRinaudo]) Get in touch with [Besedo], we're always on the lookout for Scala talent Slides backed by [remark.js](https://remarkjs.com/) and the amazing [tut](https://github.com/tpolecat/tut) --- class: center, middle # Questions? [@NicolasRinaudo]:https://twitter.com/NicolasRinaudo [Besedo]:https://twitter.com/besedo_official