principles of the play framework

Post on 16-Jul-2015

142 Views

Category:

Software

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Principles of the Play Framework

Bernhard Huemer

Why are we here?

Composition

Contextual vs. composable design

• Boundaryless extensions / flexibility

• Steep learning curve

See also: http://nealford.com/memeagora/2013/01/22/why_everyone_eventually_hates_maven.html

• Anticipated extension points

• Easy to get into / quick wins

Referential transparency

• No reassignments of variables

• No setters, no mutations

• No exceptions (no throw new …)

• No console input / output

• No network communication

• ….

"An expression is said to be referentially transparent if it can be replaced with its value without changing the behaviour of a

program."

Side-effects don’t composeprotected void renderBasket(HttpServletResponse response) throws ServletException, IOException { try (PrintWriter writer = response.getWriter()) { writer.println(“HTML for our basket.”); } } !protected void renderProductList(HttpServletResponse response) throws ServletException, IOException { try (PrintWriter writer = response.getWriter()) { writer.println(“HTML for our products list.”); } }

Best practice: Don’t assume any special context when calling these methods, it will manage its resources itself.

Side-effects don’t composeprotected void renderAll(HttpServletResponse response) throws ServletException, IOException { renderBasket(response); renderProductList(response); }

• Composing code with side-effects always requires additional “plumbing”

• in this case: close() method of the response

• Ignore the <head />, <body /> HTML issues ..

What’s wrong here?import io.Source.fromFile val it: Iterator[String] = fromFile(“foo.txt").getLines // or from the network !var result = 0 while (it.hasNext) { val line = it.next result += line.toInt }

General idea: Some part of the application produces a stream of something that we want to consume elsewhere.

What’s wrong here?import io.Source.fromFile val it: Iterator[String] = fromFile(“foo.txt").getLines // or from the network !var result = 0 while (it.hasNext) { val line = it.next result += line.toInt }

val it: Iterator[String] = fromFile("foo.txt").getLines var result = 0 it foreach { line => result += line.toInt }

val it: Iterator[String] = fromFile("foo.txt").getLines var result = 0 it foldLeft(0) { case (acc, line) => acc + line.toInt }

Problems …• Repetitive pattern (DRY principle)

• Manual pulling, possibly blocking I/O

• No error handling (sometimes we forget, right?)

• No communication with the producer (e.g. back pressure)

• Resource handling (how long do we need a resource and who is responsible for opening/closing/recovering it?)

• Missing or rather difficult composability

• What if the input source was infinite? See: http://www.slideshare.net/afwlehmann/iteratees-intro

Architecture at LinkedIn

See: http://www.slideshare.net/brikis98/play-framework-async-io-with-java-and-scala

Latency spilled over to the supposedly fast part

Clarification: Latency in one part of the network will slow down anything that depends on it directly or indirectly, it should, however, not have an impact on anything else!

Requests are functionsobject Application extends Controller { def greet(name: String) = Action { request => Ok(s"<html>Hello $name!</html>") .as("application/xhtml+xml") } }

GET /greet/:name controllers.Application.greet(name)

• Controller is not really required as base class

• Action { } is like a factory method that produces one of these functions

Play in the command linescala> controllers.App.greet(“ConFESS”) res0: play.api.mvc.Action[play.api.mvc.AnyContent] = Action(parser=BodyParser(anyContent)) !scala> controllers.App.greet(“ConFESS”).apply( play.api.test.FakeRequest() ) res1: scala.concurrent.Future[play.api.mvc.Result] = scala.concurrent.impl.Promise$KeptPromise@3ffe0466

Read-eval-print-loop (REPL): Scala, like most scripting languages, comes with an interactive shell that you can use to test your application. Play supports this tool as well.

Your application code never uses the network directly

scala> val result = Await.result( controllers.App.greet(“ConFESS”).apply( play.api.test.FakeRequest() ), atMost = 2.seconds ) result: play.api.mvc.Result = Result(200, Map( Content-Type -> application/xhtml+xml; charset=utf-8) )

• No HTTP server we needed to start

• Nothing we needed to mock *

* I will not count FakeRequest .. it’s infinitely simpler than mocking servlet requests

Side-effects are collected until the very end of the request

scala> result.body.run(Iteratee.foreach({ bytes => println(new String(bytes)) })) <html>Hello ConFESS!</html> res8: scala.concurrent.Future[Unit] = scala.concurrent.impl.Promise$DefaultPromise@3ee2e

• The framework takes care of “side-effectful” code once you returned control to it

Minimises incorrect usage“I was eventually persuaded of the need to design programming notations so as to

1. maximise the number of errors which cannot be made, or

2. if made, can be reliably detected at compile time.“

- Tony Hoare (ACM Turing Award Lecture)

• No IllegalStateExceptions in the API

• PrintWriters and OutputStreams

• Redirects after status codes / headers / etc..

• Headers after the response body was started

Composition of functionsdef Logging[A](action: Action[A]): Action[A] = Action.async(action.parser) { request => Logger.info("Calling action") action(request) }

def index = Logging { Action { Ok("Hello World") }}

No mental overhead: Simplistic example (think security, instead), but the point is: No additional concepts you need to learn!

private def futureInt(): Future[Int] = Future { intensiveComputation() } !def index = Action.async { futureInt().map({i => Ok("Got result: " + i) }) }

Small detour: Asynchronous HTTP programming

Futures: An object that acts as a proxy for a result that is initially unknown, because the computation of its value is not yet complete.

Sequential composition

def fetch(url: String): Future[Resource] = ???def getThumbnail(url: String): Future[Resource] = fetch(url) flatMap { page => fetch(page.imageLinks()(0)) }

trait Future[A] { def map[B](f: A => B): Future[B] def flatMap[B](f: A => Future[B]): Future[B] ... }

Concurrent composition

object Future { … def collect[A](fs: Seq[Future[A]]): Future[Seq[A]] def join(fs: Seq[Future[_]]): Future[Unit] def select(fs: Seq[Future[A]]) : Future[(Try[A], Seq[Future[A]])] }

“All non-trivial abstractions, to some degree, are leaky.”

- Joel Spolsky

What about streaming?

Problem with our abstraction: With this simple abstraction we’d have to have both fully in-memory: the request and the response body.

trait EssentialAction { def apply(headers: RequestHeaders): Iteratee[Array[Byte], Result] }

The ♥ of the framework (1)

RequestHeaderPath, method (GET / POST),

headers, cookies, etc., but not the request body itself!

Iteratee[A, B]

Repeatedly and asynchronously consumes instances of A to

eventually produce an instance of B

final class ResponseHeader( val status: Int, headers: Map[String, String] = Map.empty)

case class Result( header: ResponseHeader, body: Enumerator[Array[Byte]], connection: HttpConnection.Connection = HttpConnection.KeepAlive) { /* ... */ }

The ♥ of the framework (2)

Iteratee[A, B] Repeatedly and asynchronously consumes instances of A to eventually produce an instance of B

Enumerator[A] Repeatedly and asynchronously produces instances of A

“Convenient proxy factory bean superclass for proxy factory beans that create only singletons. !Manages pre- and post-interceptors (references, rather than interceptor names, as in ProxyFactoryBean) and provides consistent interface management.”

How many concepts can you keep track of?

Iteratees and Enumerators

Reactive Streams, RX, Conduits, Process, …

Enumerators

trait Enumerator[E] { def run[A](it: Iteratee[E, A]): Iteratee[E, A] } !sealed trait Input[+T] case class El[T](x: T) extends Input[T] case object Empty extends Input[Nothing] case object EOF extends Input[Nothing]

• Stream producers that push input into consumers

Iteratees

sealed trait Iteratee[I, O] !case class Done[I, O](result: O, remainingInput: Input[I]) case class Cont[I, O](k: Input[I] => Iteratee[I, O]) case class Error[I, O](t: Throwable)

• Stream consumers, consume chunks at a time

State machine for iterations: If we were to draw a state machine for iterations, this is what we would get.

Example: Counting characters

def charCounter(count: Int = 0): Iteratee[String, Int] = Cont[String, Int] { case El(str) => charCounter(count + str.length) case Empty => charCounter(count) case EOF => Done(count) }

def countChars(it: Iterator[String]): Int = { var count = 0 while (it.hasNext) { count = count + it.next.length } count }

Why bother: Iteration can be paused / resumed at any moment without losing the current state.

Example: Communication with the producer

def moreThanChunks[A](number: Int): Iteratee[A, Boolean] = Cont[A, Boolean] { case input if number <= 0 => Done(true, input) case EOF => Done(false, EOF) case El(_) => moreThanChunks(number - 1) case Empty => moreThanChunks(number) }

def moreThan[A](number: Int, it: Iterator[A]): Boolean = { var i = number while (it.hasNext) { it.next // not needed if (i <= 0) { return true } i -= 1 } return false }

Why bother: Done indicates to the producer that it can close the resource (e.g. no need to wait until the last hasNext).

Example: Enumeratordef enumerate[E](elems: Iterator[E]): Enumerator[E] = new Enumerator[E] { def run[E, A](it: Iteratee[E, A]): Iteratee[E, A] = it match { case Cont(k) => { val input = if (elems.hasNext) { El(elems.next) } else { EOF } // recursively call this method again with the next Iteratee run(k(input)) } // here we could also do resource clean-up, if necessary case _ => it } }

Complicated? Yes, I know, this isn’t particularly obvious stuff ..

Streams work just like collectionsdef charCounter(count: Int = 0): Iteratee[String, Int] = Cont[String, Int] { case El(str) => charCounter(count + str.length) case Empty => charCounter(count) case EOF => Done(count) }

def fold[A, B](acc: B)(f: (A, B) => B): Iteratee[A, B] = Cont[String, Int] { case El(a) => fold(f(a, acc))(f) case Empty => fold(acc)(f) case EOF => Done(acc) }

def charCounter: Iteratee[String, Int] = fold(0)({ case (str, count) => count + str.length }

No mental overhead: They really behave in the same way, really nothing new we’re learning here (I’m sorry ..)

Asynchronous streamssealed trait Iteratee[I, O] !case class Done[I, O](result: O, remainingInput: Input[I]) case class Cont[I, O](k: Input[I] => Future[Iteratee[I, O]]) case class Error[I, O](t: Throwable)

def fold[A,B](acc: B)(f: (A, B) => B): Iteratee[A, B] = ...def foldM[A,B](acc: B)(f: (A, B) => Future[B]): Iteratee[A, B] = ...

Composing concepts: The concept of Futures and Iteratees can be combined rather easily to provide reactive streams.

Example: Using asynchronous combinators

def expensiveComputation(str: String): Future[Int] = ???!def processExpensive: Iteratee[String, Int] = foldM(0)({ case (str, acc) => expensiveComputation(str) map { acc + _ } })

Back-pressure for free (almost): Enumerators won’t push more elements into this Iteratee than it can handle.

Composability for handling streams of data

def isAroundLocation(tweet: Tweet, loc: Location): Booleandef retweetsGreaterThan(tweet: Tweet, retweets: Int): Booleandef userForTweet(tweet: Tweet): Future[User]val tweets: Enumerator[Tweet] = // … !tweets .through(Enumeratee.filter({ tweet => isAroundLocation(tweet, Location.Vienna) }) .through(Enumeratee.filter({ tweet => retweetsGreaterThan(tweet, 50) }) .through(Enumeratee.mapM({ tweet => userForTweet(tweet) }) .run(Iteratee.foreach({ user => println(s“User $user tweeted something popular in Vienna.”) })

void onWritePossible(){ while (isReady()){ out.write("<H1>Hello</H1>"); } }

Async Servlets

• Iterative processing while stream is available

• Callbacks otherwise

What’s the problem here?

void onWritePossible(){ while (isReady()){ out.write(“<H1>Hello</H1>"); out.write(“<H1>World</H1>”); } }

Async I/O is hard: Side-effects in the API are kind of necessary, but they make it very hard to use.

boolean flag = true; !void onWritePossible(){ while (isReady()){ if (flag) { out.write(“<H1>Hello</H1>"); } else { out.write(“<H1>World</H1>"); } flag = !flag; } }

Async I/O is hard …

Enumerator.interleave( Enumerator.repeat(“Hello”), Enumerator.repeat(“World”) )

.. it doesn’t need to be though, if you make your API composable.

See: https://blogs.oracle.com/theaquarium/entry/javaone_replay_into_the_wild

Conclusion

• Build your APIs like Lego blocks

• Stay away from side effects as much as possible

• Deferred execution / interpretation allows you to do that

Book recommendation

Q & A

Bernhard Huemer @bhuemer

top related