Chris Birchall
1st Oct 2016
OOP
time
Java dev
"better Java"
discover
Scala
rediscover
FP
File I/O, external processes, DynamoDB, ...
Ansible stuff
Play controllers
DynamoDB DAOs
Event bus stuff
Domain models
Packer stuff
Guardian-specific stuff
Scheduler stuff
Play view templates
Play DI stuff
Data
Behaviour
trait Recipe {
  def generatePackerConfig(): File
}
trait ProcessRunner {
  def run(cmd: Seq[String]): Future[Int]
}
class BakeImpl(
  id: BakeId,
  buildNumber: Int,
  recipe: Recipe,
  processRunner: ProcessRunner) extends Bake {
  def execute(): Future[Int] = {
    val packerFile = 
      recipe.generatePackerConfig()
    val packerCommand = 
      buildPackerCommand(packerFile, buildNumber)
    processRunner.run(packerCommand)
  }
  
  ...
}Data
Behaviour
sealed abstract class BakeStatus
case object Running extends BakeStatus
case object Complete extends BakeStatus
case object Failed extends BakeStatus
case class Bake(
    recipe: Recipe,
    buildNumber: Int,
    status: BakeStatus,
    amiId: Option[AmiId],
    startedBy: String,
    startedAt: DateTime
)case class AmiId(value: String) extends AnyVal
case class BaseImageId(value: String) extends AnyVal
case class RecipeId(value: String) extends AnyVal
case class RoleId(value: String) extends AnyVal
case class BakeId(recipeId: RecipeId, buildNumber: Int)
Value class: no runtime overhead
object Bakes {
  def updateAmiId(bakeId: BakeId, amiId: AmiId): Unit = ...
}val bakeId = BakeId(RecipeId("my-recipe"), buildNumber = 123)
val amiId = AmiId("ami-a1b2c3d4")
Bakes.updateAmiId(amiId, bakeId) // yay, compile error!def loadRoleFromDisk(roleId: RoleId): Role = {
  ...
  if (dirNotFound)
    throw new IOException("Role directory not found")
  ...
  if (invalid)
    throw new InvalidRoleException("check ur role!")
  ...
}def loadRoleFromDisk(roleId: RoleId):
    Either[IOException,
           Either[InvalidRoleException,
                  Role]] = ...Nope!
Hmm...
sealed trait LoadRoleResult
object LoadRoleResult {
  case class Success(role: Role) extends LoadRoleResult
  case object RoleDirectoryNotFound extends LoadRoleResult
  case object RoleFormatInvalid extends LoadRoleResult
}
def loadRoleFromDisk(roleId: RoleId): LoadRoleResult = ...Downside: don't get map/flatMap for free
sealed trait LoadRoleError
object LoadRoleError {
  case object RoleDirectoryNotFound extends LoadRoleError
  case object RoleFormatInvalid extends LoadRoleError
}
def loadRoleFromDisk(roleId: RoleId): Either[LoadRoleError, Role] = ...TODO
This gives you a bunch of stuff for free
e.g. foldMap
case class Result(successes: Int, failures: Int)
implicit val rm = new Monoid[Result] {
  def empty = Result(0, 0)
  def combine(x: Result, y: Result) = Result(
    successes = x.successes + y.successes,
    failures = x.failures + y.failures
  )
}TODO
Maps the function over the list and combines the results
def processPageOfItems(page: Page[Item]): Result = ...
val pages: List[Page] = ...
import cats.instances.list._
import cats.syntax.foldable._
val overallResult: Result = 
  pages.foldMap(processPageOfItems)Behaviour
Data
object PlaybookGenerator {
  def renderRole(role: CustomisedRole): String = ...
  def generatePlaybook(recipe: Recipe): Yaml = {
    val allRoles = recipe.baseImage.builtinRoles ++ recipe.roles
    val roleStrings = allRoles.map(renderRole)
    Yaml(...)
  }
  ...
}Larger functions composed of smaller ones
Behaviour
Function hierarchy
Data
object PackerRunner {
  def createImage(bake: Bake): Future[ExitCode] = {
    val playbookYaml = 
      PlaybookGenerator.generatePlaybook(bake.recipe)
    val playbookFile = writeToTempFile(playbookYaml)
    val packerBuildConfig = PackerBuildConfigGenerator
      .generatePackerBuildConfig(bake)
    val packerConfigFile = writeToTempFile(packerBuildConfig)
    executePacker(bake, playbookFile, packerConfigFile)
  }
  ...
}Pure 😇
Pure 😇
Impure 🙈
Impure 🙈
Impure 🙈
Behaviour
Pure
functions
Effects
Data
object PackerRunner {
  def createImage(bake: Bake): Future[ExitCode] = {
    ...
  }
}class PackerRunner(bake: Bake) {
  def createImage(): Future[ExitCode] = {
    ...
  }
}Problem: You end up passing lots of params around
Partial solution: Pass them implicitly
If this gets excessive, bundle into a single context class
(c.f. ScalaCache)
object Recipes {
  def list()(implicit dynamo: Dynamo): Iterable[Recipe] = {
    ...
  }
 
  ...
}class RecipeController(...)(implicit dynamo: Dynamo) 
    extends Controller {
  def listRecipes = Action {
    Ok(views.html.recipes(Recipes.list()))
  }
  ...
}Caveat: Daniel Spiewak calls this an anti-pattern
... but it's fine if resource lifecycle == app lifecycle
Problem: how to do Dependency Injection?
Supplementary question: why do we do DI?
DAG of dependencies
Function composition
Pass functions as parameters
case class PackerInput(bake: Bake, playbookFile: Path, packerConfigFile: Path)
def createImage(bake: Bake,
  createPlaybook: Recipe => Path,
  createPackerConfig: Bake => Path,
  runPacker: PackerInput => Future[ExitCode]): Future[ExitCode] = {
  val playbookFile = createPlaybook(bake.recipe)
  val packerConfigFile = createPackerConfig(bake)
  runPacker(PackerInput(bake, playbookFile, packerConfigFile))
}Bad:
Better:
def getItem(id: Int): Option[Item] = {
  val conn = new DBConnection()
  try {
    ... 
  } finally { 
    conn.close()
  }
}def getItem(id: Int, 
            conn: DBConnection): Option[Item] = {
  ...
}Even better:
def getItem(id: Int) = { (conn: DBConnection) => 
  ...
}val run: DBConnection => Item = getItem(123)
val item: Item = run(new DBConnection())Curried function
object HandyService {
  lazy val usefulData = expensiveComputation()
}
object MyCode {
  def performanceCriticalCode() = {
    val foo = HandyService.usefulData
    ...
  }
}Is this OK?
Has somebody already taken the performance hit for us?
Data
Behaviour
Pure
functions
Effects
Glue, wiring, boilerplate
object Bakes {
  def create(...)(implicit dynamo: Dynamo): Bake = {
    val bake = Bake(...)
    val dbModel = ...
    Scanamo.put(dynamo.client)(tableName)(dbModel)
    bake
  }
  ...
}Transforms between domain models and DB models
object Bake {
  case class DbModel(
    recipeId: RecipeId,
    buildNumber: Int,
    status: BakeStatus,
    amiId: Option[AmiId],
    startedBy: String,
    startedAt: DateTime)
  import automagic._
  def domain2db(bake: Bake): DbModel =
    transform[Bake, Bake.DbModel](bake, "recipeId" -> bake.recipe.id)
  def db2domain(dbModel: DbModel, recipe: Recipe): Bake =
    transform[Bake.DbModel, Bake](dbModel, "recipe" -> recipe)
}Makes sealed classes work like enums
sealed abstract class BakeStatus extends EnumEntry
object BakeStatus extends Enum[BakeStatus] {
  val values = findValues
  case object Running extends BakeStatus
  case object Complete extends BakeStatus
  case object Failed extends BakeStatus
}val running: BakeStatus = BakeStatus.withName("Running")scala> import cats.data.Reader
import cats.data.Reader
scala> val f: Int => String = _.toString
f: Int => String = <function1>
scala> val reader = Reader(f)
reader: cats.data.Reader[Int,String] = ...
scala> reader.run(123)
res1: cats.Id[String] = 123
def getItem(id: Int) = { (conn: DBConnection) => 
  ...
}val run: DBConnection => Item = getItem(123)
val item: Item = run(new DBConnection())def getItem(id: Int): DBConnection => Item = { 
  (conn: DBConnection) =>
    // read from DB
    Item(id, "widget")
}
def renameItem(item: Item, newName: String): DBConnection => Item = { 
  (conn: DBConnection) =>
    // save to DB
    item.copy(name = newName)
}
def renameItemWithId(id: Int, newName: String): DBConnection => Item = { 
  (conn: DBConnection) =>
    val item = getItem(id)(conn)
    renameItem(item, newName)(conn)
}
val item: Item = renameItemWithId(123, "hello")(new DBConnection())import cats.data.Reader
 
def getItem(id: Int) = Reader[DBConnection, Item]{ 
  conn =>
    // read from DB
    Item(id, "widget")
}
def renameItem(item: Item, newName: String) = Reader[DBConnection, Item]{ 
  conn =>
    // save to DB
    item.copy(name = newName)
}
def renameItemWithId(id: Int, newName: String): Reader[DBConnection, Item] = {
  for {
    item <- getItem(id)
    updatedItem <- renameItem(item, newName)
  } yield updatedItem
}
val result: Item = renameItemWithId(123, "hello").run(new DBConnection())def getItem(id: Int): DBConnection => Option[Item] = { 
  (conn: DBConnection) =>
    // read from DB
    Some(Item(id, "widget"))
}
def renameItem(item: Item, newName: String): DBConnection => Item = { 
  (conn: DBConnection) =>
    // save to DB
    item.copy(name = newName)
}
def renameItemWithId(id: Int, newName: String): DBConnection => Option[Item] = { 
  (conn: DBConnection) =>
    val item = getItem(id)(conn)
    item.map(x => renameItem(x, newName)(conn))
}
val item: Option[Item] = renameItemWithId(123, "hello")(new DBConnection())import cats.data.Reader
 
def getItem(id: Int) = ReaderT[Option, DBConnection, Item]{ 
  conn =>
    // read from DB
    Some(Item(id, "widget"))
}
def renameItem(item: Item, newName: String) = Reader[DBConnection, Item]{ 
  conn =>
    // save to DB
    item.copy(name = newName)
}
def renameItemWithId(id: Int, newName: String): ReaderT[Option, DBConnection, Item] = {
  for {
    item <- getItem(id)
    updatedItem <- renameItem(item, newName).lift[Option]
  } yield updatedItem
}
val result: Option[Item] = renameItemWithId(123, "hello").run(new DBConnection())def createImage(bake: Bake, prism: Prism, eventBus: EventBus)
               (implicit packerConfig: PackerConfig): Future[ExitCode] = {
  val playbookYaml = PlaybookGenerator.generatePlaybook(bake.recipe)
  val playbookFile = TempFiles.writePlaybookToFile(playbookYaml, bake.recipe.id)
  prism.findAllAWSAccountNumbers() flatMap { awsAccountNumbers =>
    Logger.info(s"AMI will be shared with the following AWS accounts: $awsAccountNumbers")
    val packerBuildConfig = PackerBuildConfigGenerator.generatePackerBuildConfig(
      bake, playbookFile, awsAccountNumbers)
    val packerConfigFile = 
       TempFiles.writePackerConfigToFile(packerBuildConfig, bake.recipe.id)
    PackerRunner.executePacker(bake, playbookFile, packerConfigFile, eventBus)
  }
}
Starting point
def createImage(
  createPlaybook: Recipe => Path,
  findAllAWSAccounts: WSClient => Future[Seq[AWSAccount]],
  createPackerBuildConfig: (Bake, Path, Seq[AWSAccount], PackerConfig) => Path,
  executePacker: (Bake, Path, Path, EventBus) => Future[ExitCode])
  (bake: Bake, 
  eventBus: EventBus, 
  wSClient: WSClient, 
  packerConfig: PackerConfig): Future[ExitCode] = {
  val playbookFile = createPlaybook(bake.recipe)
  findAllAWSAccounts(wSClient) flatMap { awsAccounts =>
    Logger.info(s"AMI will be shared with the following AWS accounts: $awsAccounts")
    val packerConfigFile = 
      createPackerConfigToFile(bake, playbookFile, awsAccounts, packerConfig)
    PackerRunner.executePacker(bake, playbookFile, packerConfigFile, eventBus)
  }
}
Pass functions as arguments
// In AppComponents.scala
val createImage: (Bake, EventBus, WSClient, PackerConfig) = BuildService.createImage(
  createPlaybook = PlaybookGenerator.generatePlaybook andThen TempFiles.writeToFile,
  findAllAWSAccounts = ...,
  ...
) _Use currying to hide wiring
// In controller
val bake = Bakes.create(recipe, buildNumber, startedBy = request.user.fullName)
createImage(bake, eventBus, wsClient, packerConfig)class CreateImageContext(
  eventBus: EventBus, 
  wSClient: WSClient, 
  packerConfig: PackerConfig
)
def createImage(
  createPlaybook: Recipe => Path,
  findAllAWSAccounts: WSClient => Future[Seq[AWSAccount]],
  createPackerBuildConfig: (Bake, Path, Seq[AWSAccount], PackerConfig) => Path,
  executePacker: (Bake, Path, Path, EventBus) => Future[ExitCode])
  (bake: Bake)(context: CreateImageContext): Future[ExitCode] = {
  ...
}
Bundle dependencies into a context
def createImage(
  createPlaybook: Recipe => Future[Path],
  findAllAWSAccounts: ReaderT[Future, WSClient, Seq[AWSAccount]],
  createPackerBuildConfig: 
    (Bake, Path, Seq[AWSAccount]) => ReaderT[Future, PackerConfig, Path],
  executePacker: (Bake, Path, Path) => ReaderT[Future, EventBus, ExitCode])
  (bake: Bake): ReaderT[Future, CreateImageContext, ExitCode] = {
  ...
}
Reader-ify
Any function that took one of the dependencies as an argument now returns a Reader (or ReaderT)
def createImage(
  ...
  )
  (bake: Bake): ReaderT[Future, CreateImageContext, ExitCode] = {
  ...
  ... wiring to turn all the params into ReaderT[Future, CreateImageContext, A]
  ...
  for {
    playbookFile <- writePlaybook(playbookYaml)
    accounts <- findAccounts
    _ = Logger.info(s"AMI will be shared with the following AWS accounts: $accounts")
    packerConfigFile <- writePackerBuildConfig(bake, playbookFile, accounts)
    exitCode <- runPacker(PackerInput(bake, playbookFile, packerConfigFile))
  } yield {
    exitCode
  }
}
Compose the readers
type UseContext[A] = ReaderT[Future, CreateImageContext, A]
val writePlaybook: Recipe => UseContext[Path] = recipe =>
  ReaderT.lift[Future, CreateImageContext, Path](createPlaybook(recipe))
val findAccounts: UseContext[Seq[AWSAccount]] =
  findAllAWSAccountNumbers.local[CreateImageContext](_.wsClient)
val writePackerBuildConfig: (Bake, Path, Seq[AWSAccount]) => UseContext[Path] = 
  (bake, path, awsAccounts) =>
    createPackerBuildConfig(bake, path, awsAccounts)
      .local[CreateImageContext](_.packerConfig)
val runPacker: PackerInput => UseContext[ExitCode] = packerInput =>
  executePacker(packerInput).local[CreateImageContext](_.eventBus)So, about that wiring...
Monad wrangling
What we want: ReaderT[F, Input, Output]
| What we have | Necessary wrangling | 
|---|---|
| a: Output | ReaderT.pure[F, Input, Output](a) | 
| fa: F[Output] | ReaderT.lift[F, Input, Output](fa) | 
| r: Reader[Input, Output] | r.lift[F] | 
| r: ReaderT[F, X, Output] | r.local[Input](_.x) | 
val bake = ... // generate using scalacheck-shapeless!
val reader = BuildService.createImage(
  createPlaybook = _ => Future.successful(Paths.get("playbook.yml")),
  findAllAWSAccounts = ...
  createPackerBuildConfig = ...
  executePacker = _ => ReaderT.pure[Future, EventBus, ExitCode](ExitCode(0))
)(bake)
val future = reader.run(context)
whenReady(future){ exitCode => exitCode should be(ExitCode(0)) }I can now write a (quite cumbersome) test
by the way...
ReaderT[F, A, B] == Kleisli[F, A, B]
Reader[A, B] == Kleisli[Id, A, B]
So now you know what Kleisli means!
... will have to wait for another talk
Reader monad
Free monad
General FP/other