Mark Hibberd / @markhibberd
(1 + 4) + 8 = 1 + (4 + 8)
9 + 7 = 7 + 9
trait Monoid[F] { def zero: F def append(a: F, b: F): F }
// ∀ a, b in F, a + b is also in F def closure[F: Monoid](a: F, b: F): F = // enforced by type system a |+| b // ∀ a, b, c. (a + b) + c == a + (b + c) def associative[F : Monoid : Equal](a: F, b: F, c: F): Boolean = ((a |+| b) |+| c) === (a |+| (b |+| c)) // ∀ a. zero + a = a def leftIdentity[F : Monoid: Equal](a: F): Boolean = (implicitly[Monoid].zero |+| a) === a // ∀ a. a + zero = a def rightIdentity[F : Monoid: Equal](a: F): Boolean = (a |+| implicitly[Monoid].zero) === a
// Our hypothetical serialisation library: def encode[A: Codec](a: A): SuperSpeedyFormat = ??? def decode[A: Codec](v: SuperSpeedyFormat): Option[A] = ???
// Our hypothetical serialisation library: def encode[A: Codec](a: A): SuperSpeedyFormat = ??? def decode[A: Codec](v: SuperSpeedyFormat): Option[A] = ??? // forall a. decode(encode(a)) = Some(a) def roundTrip[A: Codec](a: A): Boolean = decode(encode(a)) exists (_ == a)
// Duplication for the Win!!!!! def wat(a: String, b: String) = { val aa = a.substring(0, if (a.length > 0) a.length - 1 else 0) val bb = b.substring(0, if (b.length > 0) b.length - 1 else 0) val aaa = aa.length * 111 val bbb = bb.length * 222 aaa + bbb }
def huey(request: HttpRequest): String = ??? def dewey(request: HttpRequest): Int = ??? def louie(request: HttpRequest): List[String] = ???
def huey(n: Int): (Audit, String) = ??? def dewey(s: String): (Audit, Int) = ??? def louie(l: List[String]): (Audit, Int) = ???
def huey(c: Cache, n: Int): (Cache, String) = ??? def dewey(c: Cache, s: String): (Cache, Int) = ??? def louie(c: Cache, l: List[String]): (Cache, Int) = ???
def auth(request: HttpRequest): Boolean = ??? def method(request: HttpRequest): HttpMethod = ??? def profile(request: HttpRequest)(connection: Connection): Json = ???
case class Reader[R, A](run: R => A)
def auth: Reader[HttpRequest, Boolean] = ??? def method: Reader[HttpRequest, HttpMethod] = ??? def profile(connection: Connection): Reader[HttpRequest, Json] = ???
//def auth(request: HttpRequest): Boolean ~> def auth: Reader[HttpRequest, Boolean] = ??? //def method(request: HttpRequest): HttpMethod ~> def method: Reader[HttpRequest, HttpMethod] = ??? //def profile(request: HttpRequest)(connection: Connection): Json ~> def profile(connection: Connection): Reader[HttpRequest, Json] = ???
type HttpReader[A] = Reader[HttpRequest, A] def auth: HttpReader[Boolean] = ??? def method: HttpReader[HttpMethod] = ??? def profile(connection: Connection): HttpReader[Json] = ???
case class Reader[R, A](run: R => A) { def map[B](f: A => B): Reader[R, B] = ??? def flatMap[B](f: A => Reader[R, B]): Reader[R, B] = ??? } object Reader { def value[R, A](a: => A) = ??? def ask[R]: Reader[R, R] = ??? def local[R, A](f: R => R)(reader: Reader[R, A]): Reader[R, A] = ??? }
case class Reader[R, A](run: R => A) { def map[B](f: A => B): Reader[R, B] = flatMap(a => Reader.value(f(a))) def flatMap[B](f: A => Reader[R, B]): Reader[R, B] = Reader(r => f(run(r)).run(r)) } object Reader { def value[R, A](a: => A): Reader[R, A] = Reader(_ => a) def ask[R]: Reader[R, R] = Reader(r => r) def local[R, A](f: R => R)(reader: Reader[R, A]): Reader[R, A] = Reader(r => reader.run(f(r))) }
def header(request: HttpRequest, name: String): List[String] = request.getHeaders(name).asScala.toList def method(request: HttpRequest): HttpMethod = request.getMethod def auth(request: HttpRequest): Boolean = method(request) match { case PUT | POST => header(request, "Authorization").headOption. exists(_ == "let me in") case GET => true }
def header(name: String): HttpReader[List[String]] = ask[HttpRequest] map (_.getHeaders(name).asScala.toList) def method: HttpReader[HttpMethod] = ask[HttpRequest] map (_.getMethod) def auth: HttpReader[Boolean] = for { m <- method r <- m match { case PUT | POST => header("Authorization") map (_.headOption. exists(_ == "let me in")) case GET => Reader.reader[HttpRequest, Boolean](true) } } yield r
import scalaz._, Scalaz._ def header(name: String): HttpReader[List[String]] = ask[HttpRequest] map (_.getHeaders(name).asScala.toList) def method: HttpReader[HttpMethod] = ask[HttpRequest] map (_.getMethod) def auth: HttpReader[Boolean] = method >>= (_.matchOrZero({ case PUT | POST => header("Authorization") map (_.headOption. exists(_ == "let me in")) }))
sealed trait Audit { def trail: List[Audit] = List(this) } case class AuditRead(who: String, what: String) extends Audit case class AuditWrite(who: String, what: String, mod: String) extends Audit case class AuditUnauth(who: String, what: String) extends Audit type AuditTrail = List[Audit]
def create(p: Profile): (AuditTrail, Boolean) = ??? def read(id: Long): (AuditTrail, Option[Profile]) = ??? def update(id: Long, p: Profile): (AuditTrail, Boolean) = ??? def delete(id: Long): (AuditTrail, Boolean) = ???
case class Writer[W, A](log: W, value: A)
type AuditWriter[A] = Writer[AuditTrail, A] def create(p: Profile): AuditWriter[Boolean] = ??? def read(id: Long): AuditWriter[Option[Profile]] = ??? def update(id: Long, p: Profile): AuditWriter[Boolean] = ??? def delete(id: Long): AuditWriter[Boolean] = ???
case class Writer[W, A](log: W, value A) { def map[B](f: A => B): Writer[W, B] = ??? def flatMap[B](f: A => Writer[W, B]): Writer[W, B] = ??? } object Writer { def value[W, A](a: A) = ??? def tell[W](w: W): Writer[W, Unit] = ??? def writer[W, A](a: A)(w: W): Writer[W, A] = ??? }
case class Writer[W, A](log: W, value: A) { def map[B](f: A => B): Writer[W, B] = Writer(log, f(value)) def flatMap[W: Monoid](f: A => Writer[W, B]): Writer[W, B] = { val w = f(value) Writer(log |+| w.log, w.value) } } object Writer { def value[W, A](a: => A)(implicit W: Monoid[W]) = Writer(W.zero, a) def tell[W](log: W): Writer[W, Unit] = Writer(log, ()) def writer[W, A](a: A)(w: W): Writer[W, A] = Writer(w, a) }
def create(p: Profile): (AuditTrail, Boolean) = (AuditWrite("the dude", "profile", "create").trail, true) def read(id: Long): (AuditTrail, Option[Profile]) = (AuditUnath("the dude", "profile").trail, None) def update(id: Long, p: Profile): (Auditrail, Boolean) = { val (l1, success) = delete(id) if (success) { val (l2, _) = create(p) (l1 |+| l2, true) } else (l1, false) } def delete(id: Long): (AuditTrail, Boolean) = (AuditWrite("the dude", "profile", "delete").trail, true)
def create(p: Profile): AuditWriter[Boolean] = writer(true)(AuditWrite("the dude", "profile", "create").trail) // alternative could be as above ^^^^ def read(id: Long): AuditWriter[Option[Profile]] = for { r <- Writer.value(None) _ <- Writer.tell(AuditUnauthWrite("the dude", "profile").trail) } yield r def update(id: Long, p: Profile): AuditWriter[Boolean] = for { deleted <- delete(id) result <- if (deleted) create(p) else writer(false) } yield result def delete(id: Long): AuditWriter[Boolean] = writer(true)(AuditWrite("the dude", "profile", "delete").trail)
import scalaz._, Scalaz._ def create(p: Profile): AuditWriter[Boolean] = writer(true)(AuditWrite("the dude", "profile", "create").trail) def read(id: Long): AuditWriter[Option[Profile]] = writer(None)(AuditUnauthWrite("the dude", "profile").trail) def update(id: Long, p: Profile): AuditWriter[Boolean] = delete(id).ifM(create(p), false.pure[AuditWriter]) def delete(id: Long): AuditWriter[Boolean] = writer(true)(AuditWrite("the dude", "profile", "delete").trail)
type Headers = List[(String, List[String])] case class HttpData(headers: Headers, response: Int) { def setHeader(name: String, value: String) = copy(headers = headers.filter({ case (k, _) => k != name}) ++ List((name, value: Nil))) def setResponse(response: Int) = copy(response = response) }
def length(data: HttpData, n: Int): HttpData = ??? def contentType(data: HttpData, mime: String): HttpData = ??? def json(data: HttpData, json: Json): (HttpData, String) = ??? def parse(data: HttpData, input: String): (HttpData, Option[Json]) = ???
case class State[S, A](run: S => (S, A))
type HttpState[A] = State[HttpData, A] def length(n: Int): HttpState[Unit] = ??? def contentType(mime: String): HttpState[Unit] = ??? def json(json: Json): HttpState[String] = ??? def parse(input: String): HttpState[Option[Json]] = ???
case class State[S, A](run: S => (S, A)) { def map[B](f: A => B): State[S, B] = ??? def flatMap[B](f: A => State[S, B]): State[S, B] = ??? }
object State { def value[S, A](a: => A): State[S, A] = ??? def get[S]: State[S, S] = ??? def gets[S, A](f: S => A): State[S, A] = ??? def modify[S](f: S => S): State[S, Unit] = ??? def put[S](s: S): State[S, Unit] = ??? }
case class State[S, A](run: S => (S, A)) { def map[B](f: A => B): State[S, B] = flatMap(a => State.value(f(a))) def flatMap[B](f: A => State[S, B]): State[S, B] = State(s => run(s) match { case (ss, a) => f(a).run(ss) }) }
object State { def value[S, A](a: => A): State[S, A] = State(s => (s, a)) def get[S]: State[S, S] = State(s => (s, s)) def gets[S, A](f: S => A): State[S, A] = get map f def modify[S](f: S => S): State[S, Unit] = State(s => (f(s), ())) def put[S, A](s: S): State[S, Unit] = modify(_ => s) }
import State._ def setHeader(name: String, value: String): HttpState[Unit] = modify(s => s.setHeader(name, value)) def length(n: Int): HttpState[Unit] = setHeader("Content-Length", n.toString) def contentType(mime: String): HttpState[Unit] = setHeader("Content-Type", mime)
def json(json: Json): HttpState[String] = { val result = json.pretty for { _ <- length(result.getBytes("UTF-8").length) _ <- contentType("application/json") } yield result } def parse(input: String): HttpState[Option[Json]] = for { json <- Parser.parseOption(input) _ <- if (json.isDefined) State.value(()) else State.modify(_.setResponse(400)) } yield json
import scalaz._, Scalaz._ def json(json: Json): HttpState[String] = { val result = json.pretty length(result.getBytes("UTF-8").length) >> contentType("application/json").as(result) } def parse(input: String): HttpState[Option[Json]] = for { json <- Parser.parseOption(input) _ <- unlessM(json.isDefined)( State.modify(_.setResponse(400))) } yield json
def service(request: HttpRequest, data: HttpData): (HttpData, Json) = ???
def service: HttpReader[HttpState[Json]] = ???
def service: HttpReader[HttpWriter[HttpState[Json]]] = ???
def service: HttpReader[HttpWriter[HttpState[Option[Json]]]] = ???
def user: HttpReader[Option[User]] = ??? def profile(u: User): HttpReader[Option[Profile]] = ??? def widget(u: User): HttpReader[Option[Widget]] = ??? def render(u: User, p: Profile, w: Widget): Json = ???
def service: HttpReader[Option[Json]] = for { ou <- user op <- Reader(r => for { u <- ou result <- profile(u).run(r) } yield result) ow <- Reader(r => for { u <- ou result <- widget(u).run(r) } yield result) } yield for { u <- ou p <- op w <- ou } yield render(u, p, w)
for { ... } yield for { ... } yield for { ... } yield for { ... } yield ()
[1] I first heard the "stairs" phrase from Jordon West, in his talk on transformers in scalamachine
case class Reader[R, A](run: R => A)
case class Reader[R, A](run: R => A) case class ReaderOption[R, A](run: R => Option[A])
case class Reader[R, A](run: R => A) case class ReaderOption[R, A](run: R => Option[A]) case class ReaderT[M[_], R, A](run: R => M[A])
case class Writer[W, A](run: (W, A)])
case class Writer[W, A](run: (W, A)]) case class WriterOption[W, A](run: Option[(W, A)])
case class Writer[W, A](run: (W, A)]) case class WriterOption[W, A](run: Option[(W, A)]) case class WriterT[M[_], W, A](run: M[(W, A)])
case class State[S, A](run: S => (S, A)])
case class State[S, A](run: S => (S, A)) case class StateOption[S, A](run: S => Option[(S, A)])
case class State[S, A](run: S => (S, A)] case class StateOption[S, A](run: S => Option[(S, A)] case class StateT[M[_], S, A](run: S => M[(S, A)]
trait MonadTrans[F[_[_], _]] { def lift[G[_]: Monad, A](g: G[A]): F[G, A] } // usage: g.lift[ReaderT] case class MonadTransSyntax[G[_], A](g: G[A]) { def lift[F[_[_], _]](implicit T: MonadTrans[F], M: Monad[G]): F[G, A] = T.lift(g) } object MonadTransSyntax { implicit def ToMonadTransSyntax[G[_], A](g: G[A]) = MonadTransSyntax(g) }
trait MonadReader[F[_[_], _], R] { def ask: F[R, R] def local[A](f: R => R)(reader: F[R, A]): F[R, A] }
trait MonadWriter[F[_[_], _], W] { def tell(w: W): F[W, Unit] }
trait MonadState[M[_[_], _], S] { def get: F[S, S] def gets[A](f: S => A): State[S, A] def put: F[S, Unit] def modify(f: S => S): F[S, Unit] }
def service: HttpReader[Option[Json]] = for { ou <- user op <- Reader(r => for { u <- ou result <- profile(u).run(r) } yield result) ow <- Reader(r => for { u <- ou result <- widget(u).run(r) } yield result) } yield for { u <- ou p <- op w <- ou } yield render(u, p, w)
def service: ReaderT[Option, HttpRequest, Json] = for { u <- user p <- profile(u) w <- widget(u) } yield render(u, p, w)
case class Config(baseUrl: String, https: Boolean, hmac: SecretKey) case class CaseInsensitiveString(s: String) case class Headers(v: Map[CaseInsensitiveString, Vector[String]]) trait Audit case class AuditMacFailed(data: String) extends Audit case class AuditAccessDenied(path: String) extends Audit
case class HttpRead( request: HttpRequest, reqheaders: Headers, config: Config ) case class HttpState( resheaders: Headers, status: HttpStatus ) type HttpWrite = Vector[Audit]
trait Result[A] case class Done[A](f: ChannelBuffer) extends Result[A] case class Cont[A](a: A) extends Result[A] case class Err[A](err: String \/ Throwable) extends Result[A] // Lots of other potential cases, model your control flow as data types! // case class Async[A](...) extends Result[A]
case class Http[A](run: ReaderT[ WriterT[ StateT[ Result, HttpState, _ ], HttpWrite, _ ], HttpRead, A ]
case class Http[A](run: ReaderT[ ({ type f[a] = WriterT[ ({ type g[b] = StateT[ Result, HttpState, b ] })#g, HttpWrite, a ] })#f, HttpRead, A ]
private class Level[M[_, _, _], A, B] { type t[a] = M[A, B, a] } case class Http[A](run: ReaderT[ Level[WriteT, Level[StateT, Result, HttpState ]#t, HttpWrite ]#t, HttpRead, A ] )
import scalaz._ case class Http[A]( run: RWST[Result, HttpRead, HttpWrite, HttpState, A] )
case class HttpRead[A](path: String, reqheaders: Headers, config: A) trait ResultT[M[_], A]
case class HttpRead[A](path: String, reqheaders: Headers, config: A) trait ResultT[M[_], A] type Result[A] = ResultT[Identity, A] case class Http[A, B]( run: RWST[Result, HttpRead[A], HttpWrite, HttpState, B] ) // bletch... inference failures, compiler errors, pain
case class Http[A, B](run: (HttpRead[A], HttpState) => Result[(HttpWrite, HttpState, B)] )
case class Http[A, B](run: (HttpRead[A], HttpState) => (HttpWrite, HttpState, Result[B]) )
case class Http[A](run: (HttpRead, HttpState) => (HttpWrite, HttpState, Result[A]) )
object Http { def modify(f: HttpState, HttpState): Http[Unit] = Http((_, state) => (Vector(), f(state), Cont(()))) def get: Http[HttpState] = Http((_, state) => (Vector(), state, Cont(state))) def ask: Http[HttpRead] = Http((read, state) => (Vector(), state, Cont(read))) }
object ResponseStatus { def modify(f: HttpStatus => HttpStatus): Http[Unit] = Http.modify(s => s.copy(status = f(s.status))) def get: Http[HttpStatus] = Http.get map (_.status) def set(s: HttpStatus): Http[Unit] = modify(_ => s) }
object ResponseHeaders { def modify(f: Headers => Headers): Http[Unit] = Http.modify(s => s.copy(resheaders = f(s.resheaders))) def headers: Http[Headers] = Http.get map (_.resheaders) def get(name: String): Http[Option[String]] = headers map (_.get(name)) def all(name: String): Http[List[String]] = headers map (_.all(name)) def set(name: String, value: String): Http[Unit] = modify(_ set (k, v))
}
object Errors { def send[A](content: String): Http[A] = done[A](copiedBuffer(content, UTF_8)) def errorWith[A]: HttpStatus => String => Http[A] = status => message => ResponseHeaders.set("content-type", "application/x-awesome") >> ResponseStatus.set(status) >> send(message) def error400[A] = errorWith(BAD_REQUEST) def error404[A] = errorWith(NOT_FOUND) def error500[A] = errorWith(INTERNAL_SERVER_ERROR) def error503[A] = errorWith(SERVICE_UNAVAILABLE) }
object Body { def text: Http[String] = Http.ask map (_.request.getContent.toString(UTF_8)) def json: Http[String \/ Json] = text map (Parser.parseEither) def json400: Http[Json] = json flatMap ({ case -\/(message) => Errors.error400(message) case \/-(json) = json.point[Http] }) def decode400[A: DecodeJson]: Http[A] = for { json400 flatMap (json => Decoder.decodeEither match { case -\/(message) => Errors.error400(message) case \/-(a) = json.point[Http] }) }
object Service { def createUser: Http[User] = for { user <- Body.decode400[User] _ <- unlessM(user.name.length >= 4)( Errors.error400("Username must be 4 or more characters") ) _ <- unlessM(user.password.length >= 4)( Errors.error400("Password must be 4 or more characters") ) _ <- Db.save(user) } yield user }
object Validate { def validate(condition: Boolean, message: => String): Http[Unit] = unlessM(condition)(Errors.error400(message)) }
import Validate._ object Service { def createUser: Http[User] = for { user <- Body.decode400[User] _ <- validate(user.name.length >= 4, "Username must be 4 or more characters") _ <- validate(user.password.length >= 4, "Password must be 4 or more characters") _ <- Db.save(user) } yield user }
object NaiveHttpInterpretter { def run[A]( config: Config, req: HttpRequest, http: Http[A] ): Future[HttpResponse] = Future({ val read = HttpRead(req, headers(req), config) val init = HttpState(Headers(), OK) http.run(read, init) match { case (log, state, HttpDone(f)) => ... case (log, state, HttpErr(f)) => ... case (log, state, HttpCont(f)) => ... } }) )
object SessionHttpInterpretter { def run[A]( config: Config, req: HttpRequest, http: Http[A] ): Future[HttpResponse] = Future({ // read session from cookie // validate // run // write new session back out }) )
Mark Hibberd / @markhibberd