Learning Scala as a Java/Spring-a-holic

or Learning to write clean and simple code with no magic!

@sidneyshek

Tonight's discussions

  1. Learning the fundamentals
  2. Applying it to an app
  3. Odds and ends

Learning the fundamentals

Types everywhere

Types for data, 'state machine' state, and functions

"Compile" test gives us confidence

Types provide well-defined interfaces

Algebraic Data Types:

  • Allow pattern matching to simplify problems
  • Can wrap error states for 'inline' error handling

sealed trait HttpResult[+A]

case class HttpError[A](...) extends HttpResult[A]
case class HttpDone[A](...) extends HttpResult[A]
case class HttpCont[A](...) extends HttpResult[A]

Type classes over inheritance

Type classes over inheritance

trait Keyed {
  def get: Option[Uid]
  def set(key: Uid): Keyed
}

case class MyEntity(key: Uid) extends Keyed {
  def get = ...
  def set = ...
}

case class MySecondEntity extends Keyed ...

More code initially but fewer changes when extending type class

Helps with expression problem, framework problem, ...

trait Keyed[A] {
  def get(a: A): Option[Uid]
  def set(key: Uid, a: A): A
}

object Keyed {
  def apply[A](f: A => Option[Uid], 
    g: (Uid, A) => A) = new Keyed[A] {
      def get(a: A) = f(a)
      def set(key: Uid, a: A) = g(key, a)
  }
}

case class MyEntity(key: Uid, ...)

object MyEntity {
  implicit def MyEntityToKeyed: 
    Keyed[MyEntity] = 
      Keyed(m => Some(m.key), 
        (key, m) => m.copy(key = key))
}

def func1[A: Keyed](a: A): String = ???

def func1[A](a: A)(implicit ev: Keyed[A]):
  String = ???

Applying it to an app

Architecture...stays the same

Starting at the database

// DbValue wraps value from database and possible exceptions
sealed trait DbValue[A]
case class DbSuccess[A](a: A) extends DbValue[A]
case class DbFailure[A](error: Exception) extends DbValue[A]

// A database function uses a DB connection to get a value from the database
case class Db[A] (run: Connection => DbValue[A]) {
  def map[B] (f: A => B): Db[B] = Db(i => run(i).map(f))

  def flatMap[B] (f: A => Db[B]): Db[B] = 
    Db(i => run(i).flatMap(a => f(a).run(i)))

  def lift: Service[A] = ???
}

// A 'data access object'
object PersonDb {
  def byId(param: Uid): Db[Person] = 
    Db(c => c.query(param))
}

Gluing two layers together


object PersonDb {
  def byId(param: Uid): Db[Person] = 
    Db(c => c.query(param))
}

object ItemDb {
  def byIds(id: List[Uid]): Db[List[Item]] = 
    ???
}

object FancyEntityService {
  def getApproverItemsLength(personId: Uid): 
    Service[Int] =
    PersonDb.byId(personId).flatMap(
      p => PersonDb.byId(p.approver)).flatMap(
        a => ItemDb.byIds(a.items))).map(
            items => items.length).lift

  def getApproverItemsLength2(personId: Uid): 
    Service[Int] =
      (for {
         person <- PersonDb.byId(personId)
         approver <- PersonDb.byId(
           person.approverId)
         items <- ItemDb.byIds(approver.items)
       } yield items.length).lift
}

What the...no DI?

Testability of components - by writing small well-defined functions

Cross-cutting concerns - via higher-order functions at upper layers

Configurability - by 'readers'

Example cross-cutting concern

// My controller defines routes and transaction management
class MyApi(config: Config) extends unfiltered.filter.Plan {
...

req match {
  // For a request at /profile/{id}, call PersonService.byId within a transaction
  case GET(Path(Seg("profile" :: id :: Nil))) => 
    transaction { PersonService.byId(id) }

  ... other routes ...
}

// Transaction function wraps a 'service function' in a transaction
def transaction[A](service: Service[A]) = {
  val connection = createConnection(config.connStr)
  try {
    val r = service.run(connection)
    connection.commit
    r
  } catch {
    case (e: Throwable) => { connection.rollback; throw e }
  } finally connection.close
}

}

Configuration via readers

// Provide come configurable DB connectors
case class Config(conn: Connection, ...)

// Remember the database function?
case class Db[A] 
  (run: Connection => DbValue[A]) {
  ...map and flatMap...

  // Lift creates a Service function that:
  // 1) pulls the connection out of the config
  // 2) runs the database function
  def lift: Service[A] = 
    Service(config => run(config.conn))
}

object PersonDb { 
  def byId(identity: Uid): Db[Person] = ...
}

case class Service[A] 
  (run: Config => DbValue[A]) {
    ...map and flatMap... }

object PersonService {
  def byId(identity: Uid): Service[Team] = 
    PersonDb.byId(identity).lift
}

class MyApi(config: Config) {
  ... routes ...
  def transaction[A](s: Service[A]) = {
    ...
    // Run the service with the config
    // This ends up running the database
    // function with the connection
    s.run(config)
    ...
  }
}

object Main {
  def main(...) {
    // Create the config right at the top and
    // pass it down
    new MyApi(Config(
      new RealDbConnection())).run()
  }
}

Odds and ends

Scalaz is wonderful

case class Person(identity: Uid, teams: List[Uid], ...)

object PersonDb {
  def byId(identity: Uid): Db[Person]
}

object TeamDb {
  def byId(identity: Uid): Db[Team]
}
object PersonService {
  def teamsForPerson(identity: Uid): Service[List[Team]] = 
    (for {
      person <- PersonDb.byId(identity)   // person is a Person
      teamIds = person.teams              // teamIds is a List[Uid] for teams
      teams <- ???                        // teams should be List[Team]
               // teamIds.map(TeamDb.byId(_)) gives List[Db[Team]], but I want Db[List[Team]]
      teams <- teamIds.traverse(TeamDb.byId(_)) 
    } yield teams).lift
}

Scalaz is wonderful

Really useful functions: sequence, traverse

Useful types: Monad, EitherT, Validation, NonEmptyList, Zipper

Use autocomplete by importing all types (scalaz._) and implicits (Scalaz._)
...then optimise to specific implicits in scalaz.syntax package!

Check out types in scalaz(.std) package, functions in scalaz.syntax(.std).XXXOps

Learn a bit of Haskell (e.g. http://learnyouahaskell.com/)

Scalacheck is cool

Test data generation - No more hard coded data sets!

Find edge-case bugs - Write tests/specs that run over many data sets

Specs2/Scalacheck Example


import org.specs2.{mutable, ScalaCheck}, mutable._

class PersonServiceSpec extends Specification 
  with ScalaCheck with SecurityArbitraries {

  "PersonService" should {

    "return person by login" ! prop((person: Person) => {

      (for {
        savedPerson <- PersonService.save(person)
        retrieved <- PersonService.byName(person.login)
      } yield (savedPerson, retrieved)) must returnIs(r => {
        val (savedPerson, retrieved) = r
        retrieved == Some(savedPerson)
      })

    })
  }
}

Generating random test data


trait SecurityArbitraries extends BasicTypeArbitraries {
  implicit def PersonArbitrary: Arbitrary[Person] =
    Arbitrary(for {
      login <- Gen.identifier
      name <- arbitrary[String]
      joinDate <- arbitrary[DateTime]
      password <- Gen.identifier
      teams <- genLimitedList[String](0, 20)
    } yield Person(login, name, joinDate, password, teams.distinct))
}

trait BasicTypeArbitraries {
  def genLimitedList[A: Arbitrary](min: Int = 0, max: Int = 20) =
    for {
      num <- Gen.choose(min, max)
      list <- Gen.listOfN(num, arbitrary[A])
    } yield list
}

SBT with multiple test configurations

Separate functional, integration, unit tests (src/func, src/it, src/test)


object B extends Build
{
  lazy val CustomIntegrationTest = config("it") extend(Test)
  lazy val root = Project("root", file("."))
      .configs( CustomIntegrationTest )
      .settings( inConfig(CustomIntegrationTest)(Defaults.testSettings) : _*)
      .settings(
      testGrouping in CustomIntegrationTest <<= definedTests in 
        CustomIntegrationTest map partitionTests("integrationTests"),
      testGrouping in Test <<= definedTests in 
        Test map partitionTests("unitTests"),
      parallelExecution in CustomIntegrationTest := false
    )

  def partitionTests(groupName: String)(tests: Seq[TestDefinition]) = {
    Seq(
      new Group(groupName, tests, SubProcess(Seq()))
    )
  }
}

Summary

  • The more things change, the more they stay the same
  • Think types and pattern matching
  • Think small
  • Think functional!