Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Let The Compiler Help You

Let The Compiler Help You

Talk at Scala.io 2017

Markus Hauck

November 02, 2017
Tweet

More Decks by Markus Hauck

Other Decks in Programming

Transcript

  1. Let The Compiler Help You: How To Make The Most

    Of Scala’s Typesystem Markus Hauck (@markus1189) Scala.IO 2017
  2. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion One Evening Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 2 / 61
  3. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Done!? > compile [success] Total time: 42s, completed Nov 2 14:05:42 > run ... Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 3 / 61
  4. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 4 / 61
  5. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Done!? > compile [success] Total time: 42s, completed Nov 2 14:05:42 > run ... Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 5 / 61
  6. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 6 / 61
  7. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 7 / 61
  8. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion A Better Way > compile [info] Compiling 1 Scala source to /home/brain/world-domination/target/scala-2.12/classes ... [error] ConquerWorld.scala:1337:42: type mismatch; [error] found : Int(-42) [error] required: PositiveInt [error] requiredAssets(-42) [error] ^ [error] one error found [error] (compile:compileIncremental) Compilation failed Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 8 / 61
  9. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 9 / 61
  10. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Introduction • currently: programming as a fight against the compiler • soon: work together with the compiler • tests: evidence it works • types: proof it works • you need to communicate with the compiler Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 10 / 61
  11. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion The Road Ahead • Preparations • Be Honest • Forbid it • No Garbage • Only Valid Ops Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 11 / 61
  12. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Become Friends • by default the compiler is a little shy • but once you get friends, you don’t want to go back • first thing: unmute him! • -deprecation • -unchecked • -Xfuture • -Xlint:-unused • -Ywarn-unused:imports,privates,locals • sometimes false positives, see • scalac -X • scalac -Y Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 12 / 61
  13. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Not: Documentation • documentation is not for the compiler • it is meant for humans (which are bad at it) • avoid: context sensitive reasoning • think: I know this can’t happen • use a language that the compiler understands: types Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 13 / 61
  14. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Case Study: Vending Machine • insert coin (50 cents or 1 euro) • push button for drink • abort the transaction and return money Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 14 / 61
  15. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion final class VendingMachine(id: Int) { require(id > 0 && id < 100, "Invalid identifier") private[this] var amount: Int = 0 def insertMoney(cents: Int): Unit = cents match { case 50 | 100 => amount += cents case _ => throw new IllegalArgumentException("...") } def pushButton(): Unit = if (amount == 100) { () // eject } else { throw new IllegalStateException("...") } def abort(): Int = if (amount > 0) { val res = amount; amount = 0; res } else { throw new IllegalStateException("...") } } Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 15 / 61
  16. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion A First Design • what does the compiler know? • what do you as a user of this class know without docs? Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 16 / 61
  17. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Step 1: Be Honest Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 17 / 61
  18. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Get The Max Out Of Your List Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 18 / 61
  19. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Get The Max Out Of Your List > List[Int](1, 2, 3).max res0: Int = 3 > List[Int]().max java.lang.UnsupportedOperationException: empty.max Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 19 / 61
  20. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Be Honest • not honest ⇒ no help • max pretends to always return something, which is not the case! • tell the compiler this operation can fail • Option: if no result is a result • Either: if there can be errors • Custom ADT:if Either doesn’t cut it Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 20 / 61
  21. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Be Honest • how does this apply to our case study? • insertMoney can fail • pushButton can fail • abort can fail • we will fix this later Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 21 / 61
  22. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Step 2: If not allowed, forbid it Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 22 / 61
  23. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Step 2: If not allowed, forbid it • every project has domain classes with invariants • how to verify those invariants? if (input.isValid) { ??? } else { throw new Exception("whoopsie") } class ImportantStuff(stuff: Stuff) { require(stuff.isImportant, "Not important!") } • but the compiler (and others) does not know this! Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 23 / 61
  24. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion If not allowed, forbid it case class Email(value: String) extends AnyVal { require(isValidEmail(value)) } > Email("[email protected]") res1: Email = ... > Email("Hello World!") java.lang.IllegalArgumentException: Not a valid email address Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 24 / 61
  25. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion If not allowed, forbid it • how can we improve? • for methods, the return type changed • instantiation doesn’t have one? • one solution: smart constructors / factories Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 25 / 61
  26. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion If not allowed, forbid it abstract case class Email private (...) object Email { def fromString: Either[ValidationError, Email] = ??? // exercise } > Email.fromString("[email protected]") res1: Either[ValidationError, Email] = Right("[email protected]") > Email.fromString("Hello World") res2: Either[ValidationError, Email] = Left(InvalidEmail) Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 26 / 61
  27. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Case Study • Okay, back to our case study • we want to fix: • methods that are not honest • forbid invalid instantiation Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 27 / 61
  28. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion final class VendingMachine private (id: Int) { private[this] var amount: Int = 0 def insertMoney(cents: Int): Either[InvalidCoin, Unit] = cents match { case 50 | 100 => amount += cents Right(()) case _ => Left(InvalidCoin) } def pushButton(): Either[InsufficientFunds, Unit] = if (amount == 100) { Right(()) } else { Left(InsufficientFunds) } def abort(): Either[NoChange, Int] = if (amount > 0) { val res = amount; amount = 0; Right(res) } else Left(NoChange) } Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 28 / 61
  29. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion object VendingMachine { def create(id: Int): Either[InvalidId, VendingMachine] = if (id > 0 && id < 100) { Right(new VendingMachine(id)) } else { Left(InvalidId) } } // define left sides of eithers Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 29 / 61
  30. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Case Study: Review • we got rid of all exceptions • no way to “forget” that something can fail • compiler (coworkers?) now knows quite a bit more • what else can we improve? Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 30 / 61
  31. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Step 3: Don’t accept garbage Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 31 / 61
  32. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Don’t accept garbage input • another everyday example: > List(1, 2, 3).take(-42) • Quiz: what happens? Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 32 / 61
  33. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Don’t accept garbage input • another everyday example: > List(1, 2, 3).take(-42) • Quiz: what happens? res1: List[Int] = List() Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 33 / 61
  34. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Don’t accept garbage input • our constructor and methods are quite liberal • they take almost everything! • does the validation really belong in our vending machine? • actually it is the caller’s fault! • better: don’t accept garbage and let the caller to the work • push validation to the boundaries of your system • avoid doing it over and over again in program flow Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 34 / 61
  35. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Putting It Into Practice class MailService { def sendEmail(mail: String): Either[MailValidationError, MailStatus] } class MailService { def sendEmail(mail: Email): MailStatus } Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 35 / 61
  36. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Putting It Into Practice > List(1, 2, 3).head res1: Option[Int] = Some(1) > NonEmptyList.of(1, 2, 3).head res1: Int = 1 Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 36 / 61
  37. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Putting It Into Practice sealed abstract class List[+A] { def apply(index: Int): A } sealed abstract class List[+A] { def apply(index: Natural): A } Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 37 / 61
  38. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion How To Validate • okay seems like it makes sense to do this • goal: make core logic more focused, factor out error handling • how? • use a wrapper with smart constructors • use phantom types as “tags” Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 38 / 61
  39. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Validation With Smart Constructors abstract case class Email private (...) object Email { def fromString: Either[ValidationError, Email] = ??? // exercise } • that works, but becomes cumbersome pretty quick • I know it will succeed! > Email.fromString("[email protected]") Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 39 / 61
  40. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Phantom Types • phantom type: not associated to any value • only exists at compile time • attach information to values • example: validation of user input • instead of repeated validation “to be sure”, enforce with types Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 40 / 61
  41. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Phantom Types: The Idea sealed trait Validation final abstract class Valid extends Validation final abstract class Unknown extends Validation abstract case class UserInput[A <: Validation] (value: String) object UserInput { def unknown(s: String): UserInput[Unknown] = new UserInput[Unknown](s) {} def validate(input: UserInput[Unknown]): Option[UserInput[Valid]] = ??? } Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 41 / 61
  42. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Phantom Types for Tags • Creating new classes for invariants works, but very cumbersome • But: Whenever input enters your system: require validation • Avoid validation again and again because of different flows through system and refactorings (context-free!) • annoying: static input requires validation • skipping: Tagged to avoid wrappers Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 42 / 61
  43. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Refined • the refined library to the rescue • in essence: a Refined[T, P] • actual value T + phantom type for predicate P • no wrapper, no custom phantom type (ADT) • only define your predicate or use the builtin ones • killer feature: perform validation for literals as input at compile time Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 43 / 61
  44. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Refined: Predicates • DSL to define predicates • many builtins: link • useful as documentation as well String Refined Uri String Refined Uuid String Refined MatchesRegex[W.‘"[0-9]+"‘.T] type ZeroToOne = Not[Less[W.‘0.0‘.T]] And Not[Greater[W.‘1.0‘.T]] type ValidChar = AnyOf[Digit :: Letter :: Whitespace :: HNil] Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 44 / 61
  45. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Using Refined object Refined { def index[A](xs: List[A])( i: Int Refined Positive): A = xs(i) index(List(1, 2, 3))(2) index(List(1, 2, 3))(-1) } [error] UserInput.scala:15: Predicate failed: (-1 > 0). [error] index(List(1, 2, 3))(-1) // compile error [error] ^ [error] one error found Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 45 / 61
  46. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Refined: Dynamic Input • but the macro works only for literal input • dynamic values: you still have to take care > refineV[Positive](fortyTwo) res1: Either[String, Int Refined Positive] = Right(...) > refineV[Positive](negativeInt) res2: Either[String, Int Refined Positive] = Left(...) Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 46 / 61
  47. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Case Study final class VendingMachine(id: Identifier) { private[this] var amount: Int = 0 def insertMoney(cents: Coin): Unit = ??? def pushButton(): Either[InsufficientFunds, Unit] = ??? def abort(): Either[NoChange, Int] = ??? } object VendingMachine { type Identifier = Int Refined Interval.Closed[W.‘1‘.T, W.‘100‘.T] } sealed trait Coin case object FiftyCents extends Coin case object OneEuro extends Coin Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 47 / 61
  48. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Case Study • the Identifier is checked by refined • insertMoney only allows valid coins by design • we can get rid of the Either in insertMoney • what else? Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 48 / 61
  49. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Step 4: Restrict Valid Operations Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 49 / 61
  50. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Vending State Machine Idle start Half Ready 50 - 1 - 50 - abort 50 abort 1 pushButton drink Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 50 / 61
  51. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Vending State Machine Idle start Half Ready 50 - 1 - 50 - abort 50 abort 1 pushButton drink • possible: • 50 → 50 → pushbutton • 50 → abort • 1 → pushbutton • 1 → abort • available actions depend on the implicit state • methods can return different types depending on the state, e.g., abort • so we need to check those two invariants (via types)! Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 51 / 61
  52. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion State Machine: States sealed trait VState final abstract class Idle extends VState final abstract class Half extends VState final abstract class Ready extends VState Idle start Half Ready 50 - 1 - 50 - abort 50 abort 1 pushButton drink Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 52 / 61
  53. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion State Machine: Vending Machine final class VendingMachine[S <: VState] private { def insertFirst50()(implicit ev: S =:= Idle): VendingMachine[Half] = new VendingMachine def insertSecond50()(implicit ev: S =:= Half): VendingMachine[Ready] = new VendingMachine def insertEuro()(implicit ev: S =:= Idle): VendingMachine[Ready] = new VendingMachine def pushButton()(implicit ev: S =:= Ready): (VendingMachine[Idle], Drink) = (new VendingMachine[Idle], Drink("Fizz")) def abort[T, O]()(implicit ev: Next.Aux[S, T, O]): (VendingMachine[T], O) = (new VendingMachine[T], ev.coin) } Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 53 / 61
  54. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion State Machine: Typeclass sealed abstract class Next[S <: VState] { type Next <: VState type Out <: Coin def coin: Out } final object Next { type Aux[S0 <: VState, N0 <: VState, O0 <: Coin] = Next[S0] { type Next = N0 type Out = O0 } Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 54 / 61
  55. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion State Machine: Implicit Evidence implicit val halfAbort: Next.Aux[Half, Idle, FiftyCents.type] = new Next[Half] { type Next = Idle; type Out = FiftyCents.type override val coin = FiftyCents } Idle start Half Ready 50 - 1 - 50 - abort 50 abort 1 pushButton drink Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 55 / 61
  56. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion State Machine: Implicit Evidence implicit val readyAbort: Next.Aux[Ready, Idle, OneEuro.type] = new Next[Ready] { type Next = Idle; type Out = OneEuro.type override val coin = OneEuro } Idle start Half Ready 50 - 1 - 50 - abort 50 abort 1 pushButton drink Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 56 / 61
  57. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion State Machines: Usage object VendingMachineExamples { val machine = VendingMachine.initial // machine.insertSecond50() // Cannot prove that // de.codecentric.Idle =:= de.codecentric.Half. // machine.abort() // no implicit found for Next[Idle, ???, ???] machine.insertFirst50().insertSecond50().pushButton() machine.insertEuro().pushButton() } Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 57 / 61
  58. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion State Machines: Summary • calling methods only compiles when it makes sense • eliminates our Either return types • but: restricted to immutability (change type parameters) • potentially less code than inheritance (and less weird) Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 58 / 61
  59. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 59 / 61
  60. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion The Compiler Is There To Help! THANKS! https://github.com/markus1189/scala-io-compiler-help Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 60 / 61
  61. Intro Be Honest Forbid It No Garbage Only Valid Ops

    Conclusion Bonus: Exercise • our vending machine is still quite liberal val machine: VendingMachine[Ready] = VendingMachine.initial.insertEuro() val drink1 = machine.pushButton()._2 val drink2 = machine.pushButton()._2 // ... • how could we restrict this with types? • hint: limit access to the state machine Markus Hauck (@markus1189) Let The Compiler Help You: How To Make The Most Of Scala’s Typesyste 61 / 61