Solid Type System vs Runtime Checks and Unit Tests Vladimir Pavkin - - PowerPoint PPT Presentation

solid type system
SMART_READER_LITE
LIVE PREVIEW

Solid Type System vs Runtime Checks and Unit Tests Vladimir Pavkin - - PowerPoint PPT Presentation

Solid Type System vs Runtime Checks and Unit Tests Vladimir Pavkin Plan Fail Fast concept Type Safe Patterns Fail Fast Immediate and visible failure Where can it fail? Handled runtime exceptions & assertions Unhandled runtime failure


slide-1
SLIDE 1

Solid Type System

vs

Runtime Checks and Unit Tests

Vladimir Pavkin

slide-2
SLIDE 2

Plan

Fail Fast concept Type Safe Patterns

slide-3
SLIDE 3

Fail Fast

slide-4
SLIDE 4

Immediate and visible failure

slide-5
SLIDE 5

Where can it fail?

Handled runtime exceptions & assertions Unhandled runtime failure

slide-6
SLIDE 6

Handling runtime exceptions

assert(!list.isEmpty, "List must be empty") try { str.toInt } catch { case _:Throwable => 0 }

slide-7
SLIDE 7

Where can it fail?

Runtime checks Handled runtime exceptions & assertions Unhandled runtime failure

slide-8
SLIDE 8

Runtime checks

if(container == null) if(container.isInstanceOf[ContainerA])

slide-9
SLIDE 9

Where can it fail?

Unit tests Runtime checks Handled runtime exceptions & assertions Unhandled runtime failure

slide-10
SLIDE 10

Unit tests

it should "throw NoSuchElementException for empty stack" in { val emptyStack = new Stack[Int] a [NoSuchElementException] should be thrownBy { emptyStack.pop() } } it should "not throw for empty stack" in { val stackWrapper = StackWrapper(new Stack[Int]) noException should be thrownBy stackWrapper.pop() }

slide-11
SLIDE 11

Where can it fail?

Linters Unit tests Runtime checks Handled runtime exceptions & assertions Unhandled runtime failure

slide-12
SLIDE 12

Linters

scalacOptions ++= Seq( "­Xlint", "­deprecation", "­Xfatal­warnings" ) // Wrong number of args to format() logger.error( "Failed to open %s. Error: %d" .format(file) )

slide-13
SLIDE 13

Where can it fail?

Compiler Linters Unit tests Runtime checks Handled runtime exceptions & assertions Unhandled runtime failure

slide-14
SLIDE 14

The goal

To move as much as possible to the Compiler

slide-15
SLIDE 15

How?

Just give it enough type information.

Type system to the rescue!

slide-16
SLIDE 16

Before we start...

Examples domain?

slide-17
SLIDE 17

Beefcakes!

No offense intended :)

slide-18
SLIDE 18

Ok?

def becomeAMan(douchebag: Person): Man = if(douchebag.weight > 70) new Man(douchebag.renameTo("Arny")) else null

No! Unhandled runtime failure!

becomeAMan(vpavkin).name //vpavkin.weight < 70

slide-19
SLIDE 19

NULL

slide-20
SLIDE 20

Can we handle this?

var man = becomeAMan(person) if(man != null) name else //...

slide-21
SLIDE 21

Still not nice.

code client has to clutter code with runtime checks (or fail) compiler won't complain if you forget to check

slide-22
SLIDE 22

If you control the source code, don't ever use null as a return result. It's like farting in an elevator.

Some random guy at a random Scala forum

slide-23
SLIDE 23

The problem is

insufficient type information!

Return type should be something like ManOrNull

slide-24
SLIDE 24

Option

slide-25
SLIDE 25

Option

sealed trait Option[T] case class Some[T](x: T) extends Option[T] case object None extends Option[Nothing]

slide-26
SLIDE 26

Better API

def becomeAMan(douchebag: Person): Option[Man] = if(douchebag.weight > 70) Some(new Man(douchebag.renameTo("Arny"))) else None code is documentation client has to deal with None result at compile time.

slide-27
SLIDE 27

Use wrapped value?

def firstWorkout(douchebag: Person): Option[WorkoutResult] = becomeAMan(douchebag).map(man => man.workout())

Unwrap?

def willHaveASexyGirlfriend(douchebag: Person): Boolean = becomeAMan(douchebag) match { case Some(man) => true case None => false }

slide-28
SLIDE 28

Exceptions

slide-29
SLIDE 29

Classic

def workout(man: Man): WorkoutResult = if(!man.hasShaker) throw new Error("Not enough protein!!!!111") else // do some squats or stare in the mirror for 1h

Again!

Client either uses try/catch or fails at runtime! Return type doesn't tell anything about possible failure

slide-30
SLIDE 30

Let's add some types!

slide-31
SLIDE 31

scala.Either

  • r

scalaz.\/

slide-32
SLIDE 32

Declare possible failure

slide-33
SLIDE 33

Better API

def workout(man:Man): ProteinFail \/ WorkoutResult = if(!man.hasShaker) ProteinFail("Not enough protein!!!!111").left else someWorkoutResult.right code is documentation client has to deal with errors at compile time.

slide-34
SLIDE 34

scalaz.\/

sealed trait \/[E, R] case class ­\/[E](a: E) extends (E \/ Nothing) case class \/­[R](a: R) extends (Nothing \/ R)

slide-35
SLIDE 35

Use wrapped value?

workout(man).map(result => submitToFacebook(result)) // type is // ProteinFail \/ Future[List[FacebookLike]]

Unwrap?

def tellAboutTheWorkout(w: ProteinFail \/ WorkoutResult): String = w match { case ­\/(fail) => "F**k your proteins, I can do without it" case \/­(result) => s"Dude, eat proteins, or you won't do like me: $result" }

slide-36
SLIDE 36

isInstanceOf[Man]

slide-37
SLIDE 37

isInstanceOf[T]

trait GymClient case class Man(name: String) extends GymClient case class Douchebag(name: String) extends GymClient def gymPrice(h: GymClient): Int = if(h.isInstanceOf[Man]){ val man = h.asInstanceOf[Man] if(man.name == "Arny") 0 else 100 } else { 200 }

slide-38
SLIDE 38

So runtime.

// Add another client type case class PrettyGirl(name:String) extends GymClient

It still compiles. And we charge girls as much as douchebags! It's an unhandled runtime failure!

slide-39
SLIDE 39

isInstanceOf[T]

trait GymClient case class Man(name: String) extends GymClient case class Douchebag(name: String) extends GymClient case class PrettyGirl(name:String) extends GymClient def gymPrice(h: GymClient): Int = if(h.isInstanceOf[Man]){ val man = h.asInstanceOf[Man] if(man.name == "Arny") 0 else 100 } else { 200 }

slide-40
SLIDE 40

sealed ADT

+

pattern matching

slide-41
SLIDE 41

sealed = can't be extended in

  • ther files
slide-42
SLIDE 42

Algebraic Data Type

1) Product types 2) Sum types

slide-43
SLIDE 43

Compiler knows all the possible class/trait children.

slide-44
SLIDE 44

Sealed ADT + pattern matching

sealed trait GymClient case class Man(name: String) extends GymClient case class Douchebag(name: String) extends GymClient def gymPrice(h: GymClient): Int = h match { case Man("Arny") => 0 case _: Man => 100 case _: Douchebag => 200 } // compiler checks, that match is exhaustive

slide-45
SLIDE 45

What if we add girls now?

sealed trait GymClient case class Man(name: String) extends GymClient case class Douchebag(name: String) extends GymClient case class PrettyGirl(name:String) extends GymClient def gymPrice(h: GymClient): Int = h match { case Man("Arny") => 0 case _: Man => 100 case _: Douchebag => 200 } // COMPILE ERROR! Match fails for PrettyGirl.

slide-46
SLIDE 46

Compiler saved us again!

slide-47
SLIDE 47

Tagging

slide-48
SLIDE 48

Gym DB

case class Beefcake(id: String, name: String) case class GymPass(id: String,

  • wnerId: String)
slide-49
SLIDE 49

Safer: Tags

trait JustTag def onlyTagged(value: String @@ JustTag): String = s"Tagged string: $value" // can use as plain String

  • nlyTagged("plain string") // Compiler error

val tagged = tag[JustTag]("tagged")

  • nlyTagged(tagged) // OK
slide-50
SLIDE 50

Gym DB: safer keys

case class Beefcake(id: String @@ Beefcake, name: String) case class GymPass(id: String @@ GymPass,

  • wnerId: String @@ Beefcake)
slide-51
SLIDE 51

Phantom Types

slide-52
SLIDE 52

PullUp

sealed trait PullUpState final class Up extends PullUpState final class Down extends PullUpState

slide-53
SLIDE 53

PullUp

class Beefcake[S <: PullUpState] private () { def pullUp[T >: S <: Down]() = this.asInstanceOf[Beefcake[Up]] def pullDown[T >: S <: Up]() = this.asInstanceOf[Beefcake[Down]] }

  • bject Beefcake {

def create() = new Beefcake[Down] }

slide-54
SLIDE 54

PullUp

val fresh = Beefcake.create() //Beefcake[Down] val heDidIt = fresh.pullUp() //Beefcake[Up] val notAgainPlease = heDidIt.pullUp() // CompileError: // inferred type arguments [Up] do not conform // to method pullUp's type parameter bounds

slide-55
SLIDE 55

Path Dependent Types

slide-56
SLIDE 56

The Two Gyms

class Gym(val name: String) class Beefcake(val gym: Gym){ def talkTo(other: Beefcake): Unit = println("Wazzup, Hetch!") } val normalGym = new Gym("nicefitness") val swagGym = new Gym("kimberly") val normalGuy = new Beefcake(normalGym) val swagGuy = new Beefcake(swagGym) normalGuy.talkTo(swagGuy) // we don't want that

slide-57
SLIDE 57

The Two Gyms Runtime solution

class Beefcake(val gym: Gym){ def talkTo(other: Beefcake): Unit = { // throws IllegalArgumentException if false require(this.gym == other.gym) println("Wazzup, Hetch!") } }

slide-58
SLIDE 58

Path Dependent Types

class A { class B } val a1 = new A val a2 = new A var b = new a1.B // type is a1.B b = new a2.B // Compile Error: types don't match

Type depends on the value it belongs to.

slide-59
SLIDE 59

Type safe solution

class Gym(val name: String){ class Beefcake(val gym: Gym){ def talkTo(other: Beefcake): Unit = println("Wazzup, Hetch!") } } val normalGym = new Gym("nicefitness") val swagGym = new Gym("kimberly") val normalGuy = new normalGym.Beefcake(normalGym) val swagGuy = new swagGym.Beefcake(swagGym) normalGuy.talkTo(swagGuy) // doesn't compile, Yay!

slide-60
SLIDE 60

This is not a talk about Scala type system. Not covered:

Trait composition Existential types Macros Type Classes Shapeless ...

slide-61
SLIDE 61

Q & A

slide-62
SLIDE 62

Thank you!

goo.gl/U0WYAB

PDF