Shapeless, cats, kittens

A peek at the power of generic functional programming

github, twitter: @kailuowang

Tiny program

  case class Department(
    numOfEmployees  : Int,
    budget          : Double,
    extensionNum    : Int
  )

  object Service {
    def department(id: DepartmentId): Future[Department] = {
      for {
        numOfEmployee <- HRService.numOfEmployees(id)
        budget        <- FinanceService.budget(id)
        extensionNum  <- DirectoryService.extensionNum(id)
      } yield Department(numOfEmployee, budget, extensionNum)

    }
  }



  object HRService {
    def numOfEmployees(id: DepartmentId): Future[Int] = ???
  }

  object FinanceService {
    def budget(id: DepartmentId): Future[Double] = ???
  }

  object DirectoryService {
    def extensionNum(id: DepartmentId): Future[Int] = ???
  }

What's the problem?

Sequential

Error-prone

Tiny program

  object Service {
    def department(id: DepartmentId): Future[Department] = {
      val numOfEmployeeF  = HRService.numOfEmployees(id)
      val budgetF         = FinanceService.budget(id)
      val extensionNumF   = DirectoryService.extensionNum(id)

      for {
        numOfEmployee <- numOfEmployeeF
        budget        <- budgetF
        extensionNum  <- extensionNumF
      } yield Department(
        numOfEmployee = numOfEmployee, 
        budget        = budget,
        extensionNum  = extensionNum )
    }
  }

Improved

Safe? yes. 

Performant? Yes.

DRY? not so much.

Typeclasses

  • Applicative

  • Apply

  • Foldable

  • Functor

  • Id

  • Invariant

  • Monad

  • MonadCombine

  • MonadFilter

  • Monoid

  • MonoidK

  • Semigroup

  • SemigroupK

  • Show

  • Traverse

Data types

  • Free
  • Kleisli
  • NonEmptyList/Vector
  • OptionT
  • StateT
  • WriterT
  • Validated
  • EitherT
  • Eval

cats

Instances

Cats Powered Programming

   def department(id: DepartmentId): Future[Department] = 
      ( HRService.numOfEmployees(id)
        |@| FinanceService.budget(id)
        |@| DirectoryService.extensionNum(id)) map Department.apply
    
    val department3: DeparmentId => Future[Department] = 
      (( Kleisli(HRService.numOfEmployees)
       |@| Kleisli(FinanceService.budget)
       |@| Kleisli(DirectoryService.extensionNum)) map Department.apply).run
  object Service {
    def department(id: DepartmentId): Future[Department] = {
      val numOfEmployeeF  = HRService.numOfEmployees(id)
      val budgetF         = FinanceService.budget(id)
      val extensionNumF   = DirectoryService.extensionNum(id)

      for {
        numOfEmployee <- numOfEmployeeF
        budget        <- budgetF
        extensionNum  <- extensionNumF
      } yield Department(
        numOfEmployee = numOfEmployee, 
        budget        = budget,
        extensionNum  = extensionNum )
    }
  }

V.S.

still error-prone

    val department2: DeparmentId => Future[Department] = 
      (( HRService.numOfEmployees
       |@| FinanceService.budget
       |@| DirectoryService.extensionNum) map {
        (_ |@| _ |@| _) map Department.apply }

Make your code great again

shapeless is a type class and dependent type based generic programming library for Scala

generic by shape

  Map("numberOfEmployee" -> 52, "budget" -> 10000, "extensionNumber" -> 30815)

  Map("name" -> "Mike", "sex" -> "male", "address" -> "123 ABC Ave")


  case class Department(numOfEmployees: Int, budget: Double, extensionNum: String)
  
  case class Student(name: String, sex: Sex, address: Address)

Genericity by Structure

Genericity by Shape

automatic type class derivation for Cats and generic utility functions

kittens

 List(Option(1), Option(2), Option(3)).sequence

 //gives a Option(List(1, 2, 3))

val r: Future[Department] = ???(numOfEmployees = Future(52),
                                budget         = Future(100000.0),
                                extensionNum   = Future(30185)).sequence
  

I wish I can do...

From the power of cats.Traverse

kittens

Traverse (Sequence)

    val seq = sequenceGeneric[Department]

    def department(id: DepartmentId): Future[Department] =
      seq(numOfEmployees = HRService.numOfEmployees(id),
          budget         = FinanceService.budget(id),
          extensionNum   = DirectoryService.extensionNum(id))

    val department2: String => Future[Department] =
      seq(numOfEmployees = Kleisli(HRService.numOfEmployees),
          budget         = Kleisli(FinanceService.budget),
          extensionNum   = Kleisli(DirectoryService.extensionNum)).run
    

kittens

Typeclass generation

    import cats.Semigroup, cats.syntax.semigroup._
    import semigroup._, legacy._

    val department1 = Department(23, 20000.0, 23911)
    val department2 = Department(4, 1000.0, 92711)

    val Department(totalEmployee, totalBudget, _) = department1 |+| department2
  
    assert(totalEmployee == 27)
    assert(totalBudget == 21000.0)

kittens

Typeclass generation

Miles Sabin's talk on derivation in kittens

https://www.youtube.com/watch?v=fM7x2oWSM70

Utility Functions

  • Lift

  • Traverse

  • Sequence

Title Text

 object TypeSafeConfigReader { 
   def reader[T](f: (Config, String) ⇒ T): FieldReader[Option, Config, T] 
      = FieldReader { (cfg, fieldName) ⇒
          if (cfg.hasPath(fieldName)) Option(f(cfg, fieldName))
          else None
       }

    implicit val rInt = reader(_.getInt(_))
    implicit val rDouble = reader(_.getDouble(_))
    implicit val rString = reader(_.getString(_))
    implicit val rLong = reader(_.getLong(_))
    implicit val rDuration = reader(_.getDuration(_))
    implicit val rBoolean = reader(_.getBoolean(_))
    implicit val rConfig = reader(_.getConfig(_))
 }
  

Henkan 

conversion between runtime-shaped data and staticcally-shaped data

A tiny typesafe config -> case class lib 

    case class FarmSettings(
      name: String,
      barnHouse: BarnHouseSettings,
      numOfAnimals: Int
    )

    val cfgString =
      """
        | name: "Old McDonald"
        | numOfAnimals: 5
        | barnHouse {
        |   footage: 2045.5
        |   openHours: 8h
        | }
      """.stripMargin

    val result: Option[FarmSettings] = extract[Option, FarmSettings](
      ConfigFactory.parseString(cfgString)
    )

    assert( result == Some(
      FarmSettings(
        "Old McDonald",
        BarnHouseSettings(
          2045.5d,
          Duration.ofHours(8),
          false
        ),
        5
    )))

Henkan 

conversion between runtime-shaped data and staticcally-shaped data

    case class BarnHouseSettings(
      footage: Double,
      openHours: Duration,
      parkTractor: Boolean = false
    )

    

Title Text


import java.time.LocalDate
import henkan.syntax.convert._

object caseClassConversion {

  case class Employee(name: String, 
                      address: String,
                      dateOfBirth: LocalDate, 
                      salary: Double = 50000d)

  case class UnionMember(name: String,
                         address: String,
                         dateOfBirth: LocalDate)

  employee.to[UnionMember]()


  unionMember.to[Employee]()

  //conversion with different value
  unionMember.to[Employee].set(salary = 60000.0)

}

Henkan 

conversion between case classes

default value is respected

The End

Questions
&
Option[Answers]

Kittens

By Kailuo Wang

Kittens

  • 3,333