improving correctness with types kats conf

57
Improving Correctness with Types Functional Kats September 2015

Upload: iain-hull

Post on 12-Apr-2017

264 views

Category:

Software


0 download

TRANSCRIPT

Page 1: Improving Correctness with Types Kats Conf

Improving Correctness with Types

Functional Kats September 2015

Page 2: Improving Correctness with Types Kats Conf

Iain Hull

[email protected]@IainHull

http://workday.github.io

Page 3: Improving Correctness with Types Kats Conf

Defensive Programming

Page 4: Improving Correctness with Types Kats Conf

Fail Fast

Page 5: Improving Correctness with Types Kats Conf

Design by Contract

Page 6: Improving Correctness with Types Kats Conf

What is a function ?

f

x y

Domain Range

Page 7: Improving Correctness with Types Kats Conf
Page 8: Improving Correctness with Types Kats Conf

Never use nulls … period

“Null references, my billion dollar mistake”- Tony Hoare, QCon 2009

Page 9: Improving Correctness with Types Kats Conf

All data is immutable

Almost all

Page 10: Improving Correctness with Types Kats Conf

Do not throw exceptions

Page 11: Improving Correctness with Types Kats Conf

“Bad programmers worry about the code.

Good programmers worry about data structures and their relationships.”

- Linus Torvalds

Page 12: Improving Correctness with Types Kats Conf

Wrappers

Wrapper Types

Page 13: Improving Correctness with Types Kats Conf

case class Customer(name: String, preferredCurrency: String) { require(Currency.isValid(preferredCurrency))} val customer = Customer(name = "Joe Bloggs", preferredCurrency = "SFO")

This is an airport?

Page 14: Improving Correctness with Types Kats Conf

class Currency private (val code: String) extends AnyVal object Currency { val USD: Currency = new Currency("USD") val EUR: Currency = new Currency("EUR") // ... def from(code: String): Option[Currency] = ???}

Page 15: Improving Correctness with Types Kats Conf

case class Customer(name: String, preferredCurrency: Currency)

def creditCardRate(customer: Customer): BigDecimal = { if (customer.preferredCurrency == "USD") BigDecimal("0.015") else BigDecimal("0.025")}

Always false

Page 16: Improving Correctness with Types Kats Conf

import org.scalactic.TypeCheckedTripleEquals._ case class Customer(name: String, preferredCurrency: Currency)

def creditCardRate(customer: Customer): BigDecimal = { if (customer.preferredCurrency === "USD") BigDecimal("0.015") else BigDecimal("0.025")}

Does not compile

Page 17: Improving Correctness with Types Kats Conf

val order: Order = ???val customer: Customer = ??? val creditCardCharge = order.amount + creditCardRate(customer)

Eeek this a bug

Page 18: Improving Correctness with Types Kats Conf

class MoneyAmount(val amount: BigDecimal) extends AnyVal { def + (rhs: MoneyAmount): MoneyAmount = new MoneyAmount(amount + rhs.amount)

def - (rhs: MoneyAmount): MoneyAmount = new MoneyAmount(amount - rhs.amount)  def * (rhs: Rate): MoneyAmount = new MoneyAmount(amount * rhs.size)} class Rate(val size: BigDecimal) extends AnyVal { def * (rhs: Rate): Rate = rhs * this}

Page 19: Improving Correctness with Types Kats Conf

Using wrapper types• Control the available values• Control the available operations

Page 20: Improving Correctness with Types Kats Conf

Non Empty List

Page 21: Improving Correctness with Types Kats Conf

def average(items: List[Int]): Int = items.sum / items.size

scala> average(List(5))res1: Int = 5 scala> average(List(5, 10, 15))res2: Int = 10 scala> average(List())java.lang.ArithmeticException: / by zero at .average0(<console>:8) ... 35 elided

Page 22: Improving Correctness with Types Kats Conf

import org.scalactic.Every

def average(items: Every[Int]): Int = items.sum / items.size scala> average(Every(5))res1: Int = 5 scala> average(Every(5, 10, 15))res2: Int = 10

scala> average(Every())<console>:10: error: not enough arguments for method apply: (firstElement: T, otherElements: T*)org.scalactic.Every[T] in object Every.Unspecified value parameters firstElement, otherElements. average(Every())

Page 23: Improving Correctness with Types Kats Conf

import org.scalactic.Every

def average(items: Every[Int]): Int = items.sum / items.size

def average(first: Int, rest: Int*): Int = average(Every(first, rest: _*))

scala> average(5)res1: Int = 5 scala> average(5, 10, 15)res2: Int = 10

scala> average()<console>:11: error: not enough arguments for method average: (first: Int, rest: Int*)Int.Unspecified value parameters first, rest. average()

Page 24: Improving Correctness with Types Kats Conf

def describeSomeList(items: List[Int]): String = { val ave = average(items) s"A list with average $ave"}

Does not compile

Page 25: Improving Correctness with Types Kats Conf

def maybeNonEmpty(items: List[Int]): Option[Every[Int]] = { items match { case head :: tail => Some(Every(head, tail)) case Nil => None }}

scala> maybeNonEmpty(List(5, 10, 15))res1: Option[Every[Int]] = Some(Every(5, 10, 15)) scala> maybeNonEmpty(List())res2: Option[Every[Int]] = None

Page 26: Improving Correctness with Types Kats Conf

def describeSomeList(items: List[Int]): String = { val ave = average(items) s”A list with average $ave"}

def describeSomeList(items: List[Int]): String = { maybeNonEmpty(items) match { case Some(es) => val ave = average(es) s"A list with average $ave" case None => "An empty list" }}

Does not compile

Page 27: Improving Correctness with Types Kats Conf

Algebraic Data Types

Page 28: Improving Correctness with Types Kats Conf

Agent Id Type Status Host In Use ByA01 1 Active 10.0.0.1A02 1 Failed 10.0.0.2 J01A03 2 Active 10.0.0.3 J03A04 2 Waiting 10.0.0.4

Job Id Type

Status Submitted By

Processed By

J01 1 Waiting FredJ02 1 Active Wilma A01J03 2 Complet

eBarney A03

Jobs

Agents

Page 29: Improving Correctness with Types Kats Conf

case class Agent(agentId: String, jobType: Int, host: String, port: Int, status: String, // Waiting | Active | Failed maybeLastAccessed: Option[DateTime], inUse: Boolean, maybeInUseBy: Option[String]) case class Job(referenceId: String, jobType: Int, status: String, // Waiting | Active | Complete submittedBy: String, submittedAt: DateTime, maybeStartedAt: Option[DateTime], maybeProcessedBy: Option[String], maybeCompletedAt: Option[DateTime]) 

Page 30: Improving Correctness with Types Kats Conf

case class Agent(agentId: String, jobType: JobType, address: AgentAddress,

status: AgentStatus, lastAccessed: Option[DateTime], inUse: Boolean, maybeInUseBy: Option[String]) case class Job(referenceId: String, jobType: JobType, status: JobStatus, submittedBy: User, submittedAt: DateTime, maybeStartedAt: Option[DateTime], maybeProcessedBy: Option[String], maybeCompletedAt: Option[DateTime])

Page 31: Improving Correctness with Types Kats Conf

sealed abstract class JobType(val value: Int)case object SmallJob extends JobType(1)case object LargeJob extends JobType(2)case object BatchJob extends JobType(3) sealed abstract class AgentStatus(val value: String)case object AgentWaiting extends AgentStatus("Waiting")case object AgentActive extends AgentStatus("Active")case object AgentFailed extends AgentStatus("Failed") sealed abstract class JobStatus(val value: String)case object JobWaiting extends JobStatus("Waiting")case object JobActive extends JobStatus("Active")case object JobCompelete extends JobStatus("Complete") case class AgentAddress(host: String, port: Int)case class User(name: String)

Page 32: Improving Correctness with Types Kats Conf

case class Agent(agentId: String, jobType: JobType, address: AgentAddress, status: AgentStatus, lastAccessed: Option[DateTime], inUse: Boolean, maybeInUseBy: Option[String]) case class Job(referenceId: String, jobType: JobType, status: JobStatus, submittedBy: User, submittedAt: DateTime, maybeStartedAt: Option[DateTime], maybeProcessedBy: Option[String], maybeCompletedAt: Option[DateTime])

Page 33: Improving Correctness with Types Kats Conf

import tag.@@

trait Foodef onlyFoo(value: String @@ Foo): String = s"It a foo: $value"

scala> onlyFoo("simple string")<console>:13: error: type mismatch; found : String("simple string") required: tag.@@[String,Foo] (which expands to) String with tag.Tagged[Foo] onlyFoo("simple string") ^ scala> val foo = tag[Foo]("Foo String")foo: tag.@@[String,Foo] = Foo String  scala> onlyFoo(foo)res2: String = It a foo: Foo String

def anyString(value: String): String = s"Just a string: $value” scala> anyString(foo)res6: String = Just a string: Foo String

Page 34: Improving Correctness with Types Kats Conf

case class Agent(agentId: String @@ Agent, jobType: JobType, address: AgentAddress, status: AgentStatus, lastAccessed: Option[DateTime], inUse: Boolean, maybeInUseBy: Option[String @@ Job]) case class Job(referenceId: String @@ Job, jobType: JobType, status: JobStatus, submittedBy: User, submittedAt: DateTime, maybeStartedAt: Option[DateTime], maybeProcessedBy: Option[String @@ Agent], maybeCompletedAt: Option[DateTime])

Page 35: Improving Correctness with Types Kats Conf

case class Job(referenceId: String @@ Agent, jobType: JobType, status: JobStatus, submittedBy: User, submittedAt: DateTime, maybeStartedAt: Option[DateTime], maybeProcessedBy: Option[String @@ Job], maybeCompletedAt: Option[DateTime])

Active | Complete

Page 36: Improving Correctness with Types Kats Conf

def recordCompletionMetrics(job: Job): Unit = { for( startedAt <- job.maybeStartedAt ; completedAt <- job.maybeCompletedAt ) {

writeJobEvent( event = "Completed", time = completedAt, referenceId = job.referenceId, waitingTime = (startedAt - job.submittedAt), executionTime = (completedAt - startedAt)) }}

Page 37: Improving Correctness with Types Kats Conf

def recordCompletionMetrics(job: Job): Unit = { require(job.status = JobComplete) require(job.maybeStartedAt.isDefined) require(job.maybeCompletedAt.isDefined) for (startedAt <- job.maybeStartedAt ; completedAt <- job.maybeCompletedAt ) { writeJobEvent ( event = "Completed", time = completedAt, referenceId = job.referenceId, waitingTime = (startedAt - job.submittedAt), executionTime = (completedAt - startedAt)) }}

Page 38: Improving Correctness with Types Kats Conf

sealed trait Job { def referenceId: String @@ Job def status: JobStatus def submittedBy: User def submittedAt: DateTime} case class WaitingJob(referenceId: String @@ Job, submittedBy: User, submittedAt: DateTime) extends Job { val status: JobStatus = JobWaiting}

Page 39: Improving Correctness with Types Kats Conf

sealed trait Job { def referenceId: String @@ Job def status: JobStatus def submittedBy: User def submittedAt: DateTime} case class ActiveJob(referenceId: String @@ Job, submittedBy: User, submittedAt: DateTime, startedAt: DateTime, processedBy: String @@ Agent) extends Job { val status: JobStatus = JobActive}

Page 40: Improving Correctness with Types Kats Conf

sealed trait Job { def referenceId: String @@ Job def status: JobStatus def submittedBy: User def submittedAt: DateTime}

case class CompleteJob(referenceId: String @@ Job, submittedBy: User, submittedAt: DateTime, startedAt: DateTime, processedBy: String @@ Agent, completedAt: DateTime) extends Job { val status: JobStatus = JobComplete}

Page 41: Improving Correctness with Types Kats Conf

def recordCompletionMetrics(job: CompleteJob): Unit = { writeJobEvent( event = "Completed", time = job.completedAt, referenceId = job.referenceId, waitingTime = (job.startedAt - job.submittedAt), executionTime = (job.completedAt - job.startedAt))}

Page 42: Improving Correctness with Types Kats Conf

Algebraic data types• Simply sealed traits and case classes• Exposes the shape of your data• Use this shape to control possible

states

Page 43: Improving Correctness with Types Kats Conf

Path Dependent Types

Page 44: Improving Correctness with Types Kats Conf

trait Handle { def name: Name def owner: User} trait Data { def stream: InputStream} trait Storage { def create(name: Name, owner: User, data: InputStream): Handle  def find(name: Name): Option[Handle]  def read(handle: Handle): Try[Data]}

Page 45: Improving Correctness with Types Kats Conf

case class HandleImpl(id: Long, name: Name, owner: User) extends Handle case class DataImpl(stream: InputStream) extends Data class StorageImpl extends Storage { // ...   def read(entryDesc: Handle): Try[Data] = { require(entryDesc.isInstanceOf[HandleImpl])  val impl = entryDesc.asInstanceOf[HandleImpl] dataStore.read(impl.id)}

Page 46: Improving Correctness with Types Kats Conf

val riakStorage: Storage = ??? 

val maybeData = for { handle <- riakStorage.find(someName) data <- riakStorage.read(handle).toOption} yield data

Page 47: Improving Correctness with Types Kats Conf

val riakStorage: Storage = ???val memoryStorage: Storage = ???

val maybeData = for { handle <- riakStorage.find(someName) data <- memoryStorage.read(handle).toOption} yield data

IllegalArgumentException

Page 48: Improving Correctness with Types Kats Conf

trait HandleLike { def name: Name def owner: User} trait DataLike { def stream: InputStream} trait Storage { type Handle <: HandleLike type Data <: DataLike  def create(name: Name, owner: User, data: InputStream): Handle  def find(name: Name): Option[Handle]  def read(handle: Handle): Try[Data]}

Page 49: Improving Correctness with Types Kats Conf

private[impl] class StorageImpl extends Storage { type Handle = HandleImpl type Data = DataImpl  case class HandleImpl(id: Long, name: Name, owner: User) extends HandleLike  case class DataImpl(stream: InputStream) extends DataLike  // ...   def read(entryDesc: Handle): Try[Data] = { dataStore.read(entryDesc.id) }}

Page 50: Improving Correctness with Types Kats Conf

val riakStorage: Storage = ???val memoryStorage: Storage = ???

val maybeData = for { handle <- riakStorage.find(someName) data <- memoryStorage.read(handle).toOption} yield data  

error: type mismatch; found : handle.type (with underlying type riakStorage.Handle) required: memoryStorage.Handle data <- memoryStorage.read(handle).toOption ^

Page 51: Improving Correctness with Types Kats Conf

val riakStorage1: Storage = ???val riakStorage2: Storage = ???

val maybeData = for { handle <- riakStorage1.find(someName) data <- riakStorage2.read(handle).toOption} yield data  

error: type mismatch; found : handle.type (with underlying type riakStorage1.Handle) required: riakStorage2.Handle data <- riakStorage2.read(handle).toOption ^

Page 52: Improving Correctness with Types Kats Conf

Path dependent types• Family polymorphism • Remove casts• Bound to instances not classes

Page 53: Improving Correctness with Types Kats Conf
Page 54: Improving Correctness with Types Kats Conf

More advanced techniques

• Self-recursive types• Phantom types• Shapeless

Page 55: Improving Correctness with Types Kats Conf

Defensive Programmi

ng

Fail Fast

Design by

Contract

Types

Page 56: Improving Correctness with Types Kats Conf

“A mind is like a parachute, it doesn’t work unless its open”

- Frank Zappa

Page 57: Improving Correctness with Types Kats Conf

http://workday.github.io