Let's play a game
Just some of the horrors of frameworks
A set of architectural patterns for building distributed systems
We can do better
Some examples may contain traces of Spring.
If you're allergic, please close your eyes.
class UserService @Inject()(userRepo: UserRepo) {
//...
}
vs
class UserService(userRepo: UserRepo)
@Configuration class MyConfig {
@Bean DataSource dataSource(){
//some side-effecty things allocating some DB connection pool
}
@Bean UserRepository userRepository(DataSource dataSource) {
return new UserRepositoryImpl(dataSource);
}
}
vs
object MyModule {
def make: Resource[IO, Module] =
DataSource.make[IO].map { implicit ds =>
implicit val userRepository: UserRepository[IO] =
UserRepository.make
//...
}
}
class DataSource @Inject()(
config: Config,
lifecycle: ApplicationLifecycle
) {
val db = newHikariPool(config)
lifecycle.addStopHook {
db.close()
}
}
object DataSource {
def make(config: Config): Resource[IO, DataSource] =
Resource.make(newHikariPool(config))(_.close)
.map(_.immutable)
.map(new DataSource(_))
}
@Service
class MySchedulingService {
@Scheduled(fixedRate = 1000)
void runJobAndThenWhat() {
System.out.println("Dumping data to disk...");
//try to figure out yourself
//how to change error handling strategy
throw new RuntimeException();
}
}
val schedule = Stream.sleep[F](5.seconds).repeat
val orderCountScheduledJob: Stream[F, Unit] =
Stream
.repeatEval(storage.countOrders)
.evalMap(count => log.info(s"Current order count: $count"))
.zipLeft(schedule)
fs2.co
@RestController
class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users")
public List<User> getUsers() {
return userService.getUsers();
}
}
@RestController
class UserController2 {
@GetMapping("/users")
public List<User> getUsers(@Autowired UserService userService) {
return userService.getUsers();
}
}
NPE when called
object UserRoutes {
- def make[F[_]: Sync: UserRepository]: HttpRoutes[F] = {
+ def make[F[_]: Async: ContextShift]: HttpRoutes[F] = {
val dsl = new Http4sDsl[F] {}
import dsl._
HttpRoutes.of[F] {
- case GET -> Root / "users" => UserRepository[F].findAll.map(_.asJson)
+ case GET -> Root / "users" => UserRepository.make[F].use(_.findAll).map(_.asJson)
}
}
Lack of side effects
Lack of referential transparency
for any expression `a`
given `x = a`
All occurrences of `x` in a program `p` can be replaced with `a`
//Example
val prog1 = (x, x)
val prog2 = (a, a)
//both sides equivalent
prog1 <-> prog2
val y = 2
val x = y + 1
(x, x) <-> (y + 1, y + 1)
import scala.io.StdIn
val x = StdIn.readLine()
//< Foo
val L = (x, x)
//L = (Foo, Foo)
val R = (StdIn.readLine, StdIn.readLine)
//< Foo
//< Bar
//R = (Foo, Bar)
L <!-> R
val x = Future(StdIn.readLine())
//a >> b is a.flatMap(_ => b)
(x >> x) <!-> (Future(StdIn.readLine()) >> Future(StdIn.readLine()))
val x = IO(StdIn.readLine())
//(x, x) ?? (IO(StdIn.readLine()), IO(StdIn.readLine()))
//a >> b is a.flatMap(_ => b)
(x >> x) <-> (IO(StdIn.readLine()) >> IO(StdIn.readLine()))
Manages resource acquisition and cleanup
class Lock
object ResourceSimple {
def lock(name: String): Resource[IO, Lock] = {
val acquire = IO(println(s"Acquiring $name")).map(_ => new Lock)
def cleanup(lock: Lock) = IO(println(s"Releasing $name (lock: $lock)"))
Resource.make(acquire)(cleanup)
}
}
object ResourceUsage extends IOApp {
def lock(name: String): Resource[IO, Unit] = {
val acquire = IO(println(s"Acquiring $name"))
val cleanup = IO(println(s"Releasing $name"))
Resource.make(acquire)(_ => cleanup)
}
def file(name: String): Resource[IO, FileReader] = {
val acquire = IO(println(s"Acquiring file reader: $name")) >> IO(new FileReader(name))
def cleanup(fr: FileReader) = IO(println(s"Releasing file reader: $name")) >> IO(fr.close())
Resource.make(acquire)(cleanup)
}
val megaResource: Resource[IO, FileReader] = for {
_ <- lock("lock1")
myResource <- file(".gitignore")
_ <- lock("lock2")
} yield myResource
override def run(args: List[String]): IO[ExitCode] =
megaResource
.use(readLines(_) <* IO(println("Finished reading lines\n")))
.flatMap(lines => IO(println(lines)))
.as(ExitCode.Success)
}
Acquiring lock1
Acquiring file reader: .gitignore
Acquiring lock2
Finished reading lines
Releasing lock2
Releasing file reader: .gitignore
Releasing lock1
.idea/
*.iml
*.iws
*.eml
out/
object Main extends IOApp {
val helloWorldService = HttpRoutes.of[IO] {
case GET -> Root / "hello" / name =>
Ok(s"Hello, $name.")
}.orNotFound
def run(args: List[String]): IO[ExitCode] =
BlazeServerBuilder[IO]
.bindHttp(8080, "localhost")
.withHttpApp(helloWorldService)
.resource.use(_ => IO.never)
.as(ExitCode.Success)
}
type HttpRoutes = Request => Response
type HttpRoutes = Request => IO[Response]
type HttpRoutes = Request => IO[Option[Response]]
type HttpRoutes = Request[IO] => IO[Option[Response[IO]]]
//Kleisli[F[_], A, B] ~= A => F[B]
//OptionT[F, A] ~= F[Option[A]]
type HttpRoutes =
Kleisli[OptionT[IO, ?], Request[IO], Response[IO]]
type HttpRoutes[F[_]] =
Kleisli[OptionT[F, ?], Request[F], Response[F]]
If a route is just a function
...then we can modify its input and output
Example: Response timing
object ResponseTiming {
def apply[F[_]: Clock: FlatMap](
http: HttpApp[F],
timeUnit: TimeUnit,
headerName: CaseInsensitiveString): HttpApp[F] = {
Kleisli { req =>
for {
before <- Clock[F].monotonic(timeUnit)
resp <- http(req)
after <- Clock[F].monotonic(timeUnit)
header = Header(headerName.value, s"${after - before}")
} yield resp.putHeaders(header)
}
}
}
Built-in server middleware (0.20.0-M5)
What about clients?
trait Client[F[_]] {
def run(req: Request[F]): Resource[F, Response[F]]
//...
}
object Client {
def apply[F[_]](
f: Request[F] => Resource[F, Response[F]]
): Client[F] = req => f(req)
}
Just another function
Built-in client middleware (0.20.0-M5)
Creating clients from routes - trivial server stubbing
object PaypalClient {
def make[F[_]: Sync]: Client[F] = {
val dsl = new Http4sDsl[F] {}
import dsl._
Client.fromHttpApp {
HttpRoutes
.of[F] {
case req @ POST -> Root / "make-payment" =>
for {
body <- req.decodeJson[PaymentRequest]
response <- Ok(s"Accepted payment of ${body.amount}".asJson)
} yield response
}
.orNotFound
}
}
}
Calling endpoint in test = calling a function
object RouteCallTest {
val route: HttpApp[IO] = HttpApp.notFound[IO]
val result = route(Request(method = GET, uri = Uri.uri("/home")))
result.flatMap(_.bodyAsText.compile.foldMonoid).map {
_ shouldBe "Not found"
}
}
case class Country(name: String, capital: String)
def countryById(id: CountryId): IO[Option[Country]] =
sql"""select c.name, c.capital
from countries
where c.id = $id"""
.query[Country]
.option
.transact(transactor)
Transactor
val transactor: Resource[IO, HikariTransactor[IO]] = for {
connectEc <- ExecutionContexts.fixedThreadPool[IO](size = 10)
transactEc <- ExecutionContexts.cachedThreadPool[IO]
xa <- HikariTransactor.newHikariTransactor[IO](
"org.postgresql.Driver",
"jdbc:postgresql://localhost/postgres",
"postgres",
"postgres",
connectEc,
transactEc)
} yield xa
Query
def countryById(id: CountryId): Query0[Country] =
sql"""select c.name, c.capital
from countries
where c.id = $id""".query[Country]
val countries: Query0[Country] =
sql"select c.name, c.capital from countries"
.query[Country]
val country1: ConnectionIO[Option[Country]] =
countryById(CountryId(1L)).option
val countriesStream: Stream[ConnectionIO, Country] =
countries.stream
ConnectionIO -> IO
transactor.use { xa =>
country1
.transact(xa)
.flatMap(putStrLn(_)) // Some(Country(...))
}
End-to-end streaming with http4s
object DoobieMain extends IOApp with Http4sDsl[IO] {
val streamCountries: Stream[ConnectionIO, Country] =
sql"select c.name, c.capital from countries".query[Country].stream
def countriesService(xa: Transactor[IO]): HttpApp[IO] =
HttpRoutes
.of[IO] {
case GET -> Root / "countries" =>
Ok(streamCountries.transact(xa).map(_.asJson))
}
.orNotFound
override def run(args: List[String]): IO[ExitCode] = {
val server = for {
xa <- transactor
_ <- BlazeServerBuilder[IO].withHttpApp(countriesService(xa)).resource
} yield ()
//never completes normally (so the server won't shut down)
server.use(_ => IO.never).as(ExitCode.Success)
}
}
Query typechecking
class AnalysisTestScalaCheck extends FunSuite with Matchers with IOChecker {
val transactor = Transactor.fromDriverManager[IO](
"org.postgresql.Driver", "jdbc:postgresql:world", "postgres", ""
)
test("countryById") { check(countryById(CountryId(1L))) }
test("countries") { check(countries) }
}
Slides: https://git.io/fhsW5
Code: https://git.io/fhoVH
Get in touch
(Read my blog! blog.kubukoz.com)