├── .gitignore ├── README.md ├── build.sbt └── src └── main └── scala └── typeclasses ├── v1_humanlike ├── BehavesLikeHuman.scala └── HumanLikeDriver.scala ├── v2_pizza2string ├── DriverV2.scala ├── Models.scala └── ToString.scala └── v3_order2string ├── DriverV3.scala ├── Models.scala └── ToString.scala /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | project/ 3 | target/ 4 | build/ 5 | .cache 6 | .cache-main 7 | .classpath 8 | .history 9 | .project 10 | .scala_dependencies 11 | .settings 12 | .worksheet 13 | .DS_Store 14 | *.class 15 | *.log 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Source code for the Scala “type class” lessons 2 | ============================================== 3 | 4 | This repository contains the source code for the 5 | “type class” lessons in my book, 6 | [Learning Functional Programming in 7 | Scala](https://alvinalexander.com/scala/learning-functional-programming-in-scala-book) 8 | 9 | 10 | Alvin Alexander 11 | https://alvinalexander.com 12 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "TypeClasses" 2 | 3 | version := "1.0" 4 | 5 | scalaVersion := "2.12.2" 6 | 7 | scalacOptions += "-deprecation" 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/scala/typeclasses/v1_humanlike/BehavesLikeHuman.scala: -------------------------------------------------------------------------------- 1 | package typeclasses.v1_humanlike 2 | 3 | // Step 1: define a behavior in a trait that takes a generic type 4 | trait BehavesLikeHuman[A] { 5 | def speak(a: A): Unit 6 | def eatHumanFood(a: A): Unit 7 | } 8 | 9 | // Step 2: create instances for the types we care about (Dog, in this case) 10 | object BehavesLikeHumanInstances { 11 | // implement an instance for a Dog 12 | implicit val dogBehavesLikeHuman = new BehavesLikeHuman[Dog] { 13 | def speak(dog: Dog): Unit = { 14 | println(s"I'm a Dog, my name is ${dog.name}") 15 | } 16 | def eatHumanFood(dog: Dog): Unit = { 17 | println(s"I ate the food you left on the table. It was good.") 18 | } 19 | } 20 | } 21 | 22 | // Step 3a: add functions that can be used on a Dog instance; use like `speak(dog)` 23 | object BehavesLikeHuman { 24 | def speak[A](a: A)(implicit behavesLikeHumanInstance: BehavesLikeHuman[A]): Unit = { 25 | behavesLikeHumanInstance.speak(a) 26 | } 27 | def eatHumanFood[A](a: A)(implicit behavesLikeHumanInstance: BehavesLikeHuman[A]): Unit = { 28 | behavesLikeHumanInstance.eatHumanFood(a) 29 | } 30 | } 31 | 32 | // Step 3b: add methods to dog class; use like `dog.speak` 33 | object BehavesLikeHumanSyntax { 34 | implicit class BehavesLikeHumanOps[A](value: A) { 35 | def speak(implicit behavesLikeHumanInstance: BehavesLikeHuman[A]): Unit = { 36 | behavesLikeHumanInstance.speak(value) 37 | } 38 | def eatHumanFood(implicit behavesLikeHumanInstance: BehavesLikeHuman[A]): Unit = { 39 | behavesLikeHumanInstance.eatHumanFood(value) 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/scala/typeclasses/v1_humanlike/HumanLikeDriver.scala: -------------------------------------------------------------------------------- 1 | package typeclasses.v1_humanlike 2 | 3 | sealed trait Animal 4 | final case class Dog(name: String) extends Animal 5 | final case class Cat(name: String) extends Animal 6 | final case class Bird(name: String) extends Animal 7 | 8 | object HumanLikeDriver extends App { 9 | 10 | import BehavesLikeHumanInstances.dogBehavesLikeHuman 11 | 12 | val rover = Dog("Rover") 13 | 14 | // (3a) apply the functions to the Dog instance 15 | //import BehavesLikeHuman.{speak, eatHumanFood} //can also import the functions 16 | BehavesLikeHuman.speak(rover)(dogBehavesLikeHuman) 17 | BehavesLikeHuman.eatHumanFood(rover)(dogBehavesLikeHuman) 18 | 19 | // (3b) import the function, and you can call it on the Dog instance 20 | import BehavesLikeHumanSyntax.BehavesLikeHumanOps 21 | rover.speak 22 | rover.eatHumanFood 23 | 24 | // the function isn't implemented for a Cat or Bird, so these won't work 25 | //speak(Cat("Garfield")) 26 | //speak(Bird("Polly")) 27 | 28 | } 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/scala/typeclasses/v2_pizza2string/DriverV2.scala: -------------------------------------------------------------------------------- 1 | package typeclasses.v2_pizza2string 2 | 3 | object DriverV2 extends App { 4 | 5 | import ToStringInstances.pizzaAsString 6 | import ToStringSyntax._ 7 | 8 | val p = Pizza(LargeCrustSize, ThinCrustType, Seq(Cheese, Pepperoni, Sausage)) 9 | 10 | println("\nHere's the pizza") 11 | println("----------------") 12 | 13 | // OPTION 1 14 | println(ToString.asString(p)) 15 | 16 | // OPTION 2 17 | println(p.asString) 18 | 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/scala/typeclasses/v2_pizza2string/Models.scala: -------------------------------------------------------------------------------- 1 | package typeclasses.v2_pizza2string 2 | 3 | sealed trait Topping 4 | case object Cheese extends Topping 5 | case object Pepperoni extends Topping 6 | case object Sausage extends Topping 7 | case object Mushrooms extends Topping 8 | case object Onions extends Topping 9 | 10 | sealed trait CrustSize 11 | case object SmallCrustSize extends CrustSize 12 | case object MediumCrustSize extends CrustSize 13 | case object LargeCrustSize extends CrustSize 14 | 15 | sealed trait CrustType 16 | case object RegularCrustType extends CrustType 17 | case object ThinCrustType extends CrustType 18 | case object ThickCrustType extends CrustType 19 | 20 | case class Pizza ( 21 | crustSize: CrustSize, 22 | crustType: CrustType, 23 | toppings: Seq[Topping] 24 | ) 25 | -------------------------------------------------------------------------------- /src/main/scala/typeclasses/v2_pizza2string/ToString.scala: -------------------------------------------------------------------------------- 1 | package typeclasses.v2_pizza2string 2 | 3 | // (1) the Type Class 4 | trait ToString[A] { 5 | def toString(a: A): String 6 | } 7 | 8 | 9 | // (2) type class instances we want 10 | // gory "to string" functions here 11 | object ToStringInstances { 12 | implicit val pizzaAsString = new ToString[Pizza] { 13 | def toString(p: Pizza): String = { 14 | s"""|Pizza(${p.crustSize}, ${p.crustType}), 15 | | toppings = ${p.toppings}""".stripMargin 16 | } 17 | } 18 | } 19 | 20 | 21 | // (3a) 22 | object ToString { 23 | def asString[A](a: A)(implicit toStringInstance: ToString[A]): String = { 24 | toStringInstance.toString(a) 25 | } 26 | } 27 | 28 | // (3b) 29 | object ToStringSyntax { 30 | implicit class ToStringOps[A](value: A) { 31 | def asString(implicit toStringInstance: ToString[A]): String = { 32 | toStringInstance.toString(value) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala/typeclasses/v3_order2string/DriverV3.scala: -------------------------------------------------------------------------------- 1 | package typeclasses.v3_order2string 2 | 3 | object DriverV3 extends App { 4 | 5 | import ToStringInstances.{orderAsString, pizzaAsString} 6 | import ToStringSyntax._ 7 | 8 | println("\nHere's the pizza") 9 | println("----------------") 10 | val p = Pizza(LargeCrustSize, ThinCrustType, Seq(Cheese, Pepperoni, Sausage)) 11 | 12 | // OPTION 1 13 | println(ToString.asString(p)) 14 | 15 | // OPTION 2 16 | println(p.asString) 17 | 18 | 19 | /** 20 | * now do the same thing for an Order 21 | */ 22 | val address = Address( 23 | "1 Main Street", 24 | None, 25 | "Talkeetna", 26 | "AK", 27 | "99676" 28 | ) 29 | 30 | val customer = Customer( 31 | "Alvin Alexander", 32 | "907-555-1212", 33 | address 34 | ) 35 | 36 | val order = Order( 37 | Seq(p), 38 | customer 39 | ) 40 | 41 | println("\nHere's the Order") 42 | println("----------------") 43 | println(order.asString) 44 | 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/scala/typeclasses/v3_order2string/Models.scala: -------------------------------------------------------------------------------- 1 | package typeclasses.v3_order2string 2 | 3 | sealed trait Topping 4 | case object Cheese extends Topping 5 | case object Pepperoni extends Topping 6 | case object Sausage extends Topping 7 | case object Mushrooms extends Topping 8 | case object Onions extends Topping 9 | 10 | sealed trait CrustSize 11 | case object SmallCrustSize extends CrustSize 12 | case object MediumCrustSize extends CrustSize 13 | case object LargeCrustSize extends CrustSize 14 | 15 | sealed trait CrustType 16 | case object RegularCrustType extends CrustType 17 | case object ThinCrustType extends CrustType 18 | case object ThickCrustType extends CrustType 19 | 20 | // clean model here 21 | case class Pizza ( 22 | crustSize: CrustSize, 23 | crustType: CrustType, 24 | toppings: Seq[Topping] 25 | ) 26 | 27 | case class Order ( 28 | pizzas: Seq[Pizza], 29 | customer: Customer 30 | ) 31 | 32 | case class Customer ( 33 | name: String, 34 | phone: String, 35 | address: Address 36 | ) 37 | 38 | case class Address ( 39 | street1: String, 40 | street2: Option[String], 41 | city: String, 42 | state: String, 43 | zipCode: String 44 | ) 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/scala/typeclasses/v3_order2string/ToString.scala: -------------------------------------------------------------------------------- 1 | package typeclasses.v3_order2string 2 | 3 | 4 | // (1) the Type Class 5 | trait ToString[A] { 6 | def asString(a: A): String 7 | } 8 | 9 | 10 | // (2) type class instances we want 11 | // gory "to string" functions here 12 | object ToStringInstances { 13 | 14 | implicit val pizzaAsString = new ToString[Pizza] { 15 | def asString(p: Pizza): String = pizzaAsAStringHelper(p) 16 | } 17 | 18 | implicit val orderAsString = new ToString[Order] { 19 | def asString(o: Order): String = { 20 | val orderString =s"""|Customer: (${o.customer})""".stripMargin 21 | 22 | val pizzaString = for { 23 | p <- o.pizzas 24 | } yield pizzaAsAStringHelper(p) 25 | 26 | orderString + "\n" + pizzaString 27 | } 28 | } 29 | 30 | // a little helper method that both instances can use to print a pizza 31 | private def pizzaAsAStringHelper(p: Pizza): String = { 32 | s"""|Pizza(${p.crustSize}, ${p.crustType}), 33 | | toppings = ${p.toppings}""".stripMargin 34 | } 35 | 36 | } 37 | 38 | // (3a) 39 | object ToString { 40 | def asString[A](a: A)(implicit toStringInstance: ToString[A]): String = { 41 | toStringInstance.asString(a) 42 | } 43 | } 44 | 45 | // (3b) 46 | object ToStringSyntax { 47 | 48 | implicit class ToStringOps[A](value: A) { 49 | def asString(implicit toStringInstance: ToString[A]): String = { 50 | toStringInstance.asString(value) 51 | } 52 | } 53 | 54 | } 55 | --------------------------------------------------------------------------------