Julien Richard-Foy
Curry On – July 19th, 2016
http://julienrf.github.io/curry-on-2016
val readItem = Router.from {
case GET(p"/item/$id") =>
itemsRepository.lookup(id)
.map(item => Ok(representation(item)))
}
def eventualItem(id: String): Future[Item] =
httpClient
.url(s"http://eshop.com/item/${urlEncode(id)}")
.get()
.map(response => decodeItemFromJsonRepresentation(response))
$ curl http://eshop.com/item
{
"data": [
{
"href": "http://eshop.com/item/123abc",
"format": {
"name": "string",
"price": "decimal"
}
}
]
}
/foo/bar
, /item
, /item/123abc
;foo
, bar
) that can be chained (like in foo/bar
).trait PathAlg[A] {
def segment(value: String): A
def chained(first: A, second: A): A
}
PathAlg
is an object algebra interface, A
is its carrier type ;segment
and chained
are path constructors ;trait PathAlg[A] {
def segment(value: String): A
def chained(first: A, second: A): A
}
// defines the URL path “item/123abc”
def someItemPath[A](alg: PathAlg[A]): A = {
import alg._
chained(segment("item"), segment("123abc"))
}
object PathClient extends PathAlg[String] {
def segment(value: String): String = URLEncoder.encode(value)
def chained(first: String, second: String): String = s"$first/$second"
}
PathClient
is an object algebra that uses String
as the concrete representation for the carrier type ;String
;someItemPath(PathClient) // "item/123abc"
object PathRouting extends PathAlg[String => Boolean] {
def segment(value: String): String => Boolean =
incoming => URLDecoder.decode(incoming) == value
def chained(fst: String => Boolean, snd: String => Boolean): String => Boolean =
incoming => {
val i = incoming.indexOf("/")
i >= 0 && fst(incoming.take(i)) && snd(incoming.drop(i + 1))
}
}
val isSomeItem: String => Boolean = someItemPath(PathRouting)
isSomeItem("foo/bar") // false
isSomeItem("item/123abc") // true
String
out of a path definition (to build an URL to perform a request on) ;trait UrlAlg[A, B, C] {
def segment(value: String): A
def chained(first: A, second: A): A
def queryStringParameter(name: String): B
def url(path: A, queryString: B): C
}
trait UrlAlg {
type Path
def segment(value: String): Path
def chained(first: Path, second: Path): Path
type QueryString
def queryStringParameter(name: String): QueryString
type Url
def url(path: Path, queryString: QueryString): Url
}
def someItemPath[A, B, C](alg: UrlAlg[A, B, C]): A = {
import alg._
chained(segment("item"), segment("123abc"))
}
def someItemPath(alg: UrlAlg): alg.Path = {
import alg._
chained(segment("item"), segment("123abc"))
}
trait PathAlg {
type Path <: PathSyntax
def segment(value: String): Path
def chained(first: Path, second: Path): Path
trait PathSyntax {
final def / (that: String): Path = chained(this, segment(that))
}
val path: Path = segment("")
}
// defines the URL path “/item/123abc”
def someItemPath(alg: PathAlg): alg.Path = {
import alg._
path / "item" / "123abc"
}
/item/abc123
or /item/456def
, but not /item/{id}
(where {id}
is any string segment) ;Path
a type constructor.trait PathAlg {
type Path[A]
def constSegment(value: String): Path[Unit]
def stringSegment: Path[String]
def integerSegment: Path[Int]
def chained[A, B](first: Path[A], second: Path[B]): Path[(A, B)]
}
// “{name}/{id}”
def item(alg: PathAlg): alg.Path[(String, Int)] = {
import alg._
stringSegment / integerSegment
}
object PathClient extends PathAlg {
type Path[A] = A => String
…
}
item(PathClient)(("foo", 42)) // "foo/42"
A
is the information needed to build a pathobject PathRouting extends PathAlg {
type Path[A] = String => Option[A]
…
}
item(PathRouting)("foo/42") // Some(("foo", 42))
item(PathRouting)("foo/bar") // None
A
is the information that is extracted from the path of an incoming requesttrait EndpointAlg extends UrlAlg {
type Request
def get(url: Url): Request
}
val someItemRequest =
get(url(path / "item" / "123abc"))
trait UrlDocumentation extends UrlAlg {
type Path = Html
type QueryString = Html
type Url = Html
def segment(value: String) = …
def chained(first: Path, second: Path) = …
def queryStringParameter(name: String) = …
def url(path: Path, queryString: QueryString) = …
}
PathAlg
that uses a type constructor as a carrier typetrait PathAlg {
type Path[A]
def constSegment(value: String): Path[Unit]
def stringSegment: Path[String]
def integerSegment: Path[Int]
def chained[A, B](fst: Path[A], snd: Path[B]): Path[(A, B)]
}
trait PathApplicativeAlg extends PathAlg {
val pathApplicative: Applicative[Path]
import pathApplicative._
def chained[A, B](fst: Path[A], snd: Path[B]) =
map2(fst, snd)((a, b) => (a, b))
}
trait PathMonadAlg extends PathApplicativeAlg {
val pathMonad: Monad[Path]
import pathMonad._
lazy val pathApplicative = pathMonad
}
PathMonadAlg
extend PathApplicativeAlg
PathRouting
interpretertrait PathRouting extends PathMonadAlg {
type Path[A] = String => Option[(String, A)]
val pathMonad =
new Monad[Path] {
def pure[A](a: A): Path[A] = s => Some((s, a))
def flatMap[A, B](path: Path[A])(f: A => Path[B]): Path[B] =
(s: String) => path(s).flatMap { case (ss, a) => f(a)(ss) }
}
}
Path[A]
is a state monad that attempts to extract some A
from a path value, and returns the remaining path along with this A
PathDocumentation
interpretertrait PathDocumentation extends PathApplicativeAlg {
type Path[A] = String
val pathApplicative =
new Applicative[Path] {
def pure[A](a: A): Path[A] = ""
def map[A, B](path: Path[A])(f: A => B): Path[B] = path
def ap[A, B](f: Path[A => B], path: Path[A]): Path[B] = s"$f/$path"
}
def constSegment(value: String) = value
val integerSegment = "{number}"
val stringSegment = "{string}"
}
Path[A]
is a simple String
, so we can not implement a Monad[Path]
;Applicative[Path]
, though.def program1(alg: PathApplicativeAlg) = …
program1
is limited to the expressive power of applicative functors, but it accepts more interpreters (PathRouting
and PathDocumentation
) ;def program2(alg: PathMonadAlg) = …
program2
can leverage the expressive power of monads, but it accepts less interpreters (only PathRouting
).trait UrlAlg {
type Path <: Url
def segment(value: String): Path
def chained(first: Path, second: Path): Path
type QueryString
def queryStringParameter(name: String): QueryString
type Url
def url(path: Path, queryString: QueryString): Url
}
trait UrlAlg {
type Path <: Url
def segment(value: String): Path
def chained(first: Path, second: Path): Path
…
type Url
…
}
val readItem = Router.from {
case GET(p"/item/$id") =>
lookupAndRenderItem(id)
}
def eventualItem(id: String): Future[Item] =
httpClient
.url(s"http://eshop.com/item/${urlEncode(id)}")
.get()
.map(response => decodeItemFromJsonRepresentation(response))
def readItemRequest(alg: EndpointAlg): alg.Request[String] = {
import alg._
get(path / "item" / stringSegment)
}
Before:
val readItem = Router.from {
case GET(p"/item/$id") =>
lookupAndRenderItem(id)
}
After:
val readItem = Router.from {
readItemRequest(EndpointRouting).returning { id =>
lookupAndRenderItem(id)
}
}
Before:
def eventualItem(id: String): Future[Item] =
httpClient
.url(s"http://eshop.com/item/${urlEncode(id)}")
.get()
.map(response => decodeItemFromJsonRepresentation(response))
After:
def eventualItem(id: String): Future[Item] =
readItemRequest(EndpointClient)(id)
.map(response => decodeItemFromJsonRepresentation(response))
readItemRequest(EndpointDocumentation) // "GET /item/{string}"
def readItemRequest(alg: EndpointAlg): alg.Request[String] = {
import alg._
get(path / "item" / stringSegment)
}
def createItemRequest(alg: EndpointAlg): alg: Request[CreateItem] = {
import alg._
post(path / "item", jsonRequest[CreateItem])
}
trait ItemEndpoints extends EndpointAlg {
val readItemRequest: Request[String] =
get(path / "item" / stringSegment)
val createItemRequest: Request[CreateItem] =
post(path / "item", jsonRequest[CreateItem])
}
object ItemClient extends ItemEndpoints with EndpointClient
sealed trait Path
case class Segment(value: String) extends Path
case class Chained(first: Path, second: Path) extends Path
Segment
and Chained
.sealed trait Path
case class Segment(value: String) extends Path
case class Chained(first: Path, second: Path) extends Path
val someItemPath: Path =
Chained(Segment("item"), Segment("123abc"))
def fold[A](s: String => A, c: (A, A) => A): Path => A = {
case Segment(value) => s(value)
case Chained(fst, snd) => c(fold(s, c)(fst), fold(s, c)(snd))
}
val client: Path => String =
fold[String](URLEncoder.encode, (fst, snd) => s"$fst/$snd")
val server: Path => String => Boolean =
fold[String => Boolean](
value => incoming => URLDecoder.decode(incoming) == value,
(fst, snd) => incoming => {
val i = incoming.indexOf("/")
i >= 0 && fst(incoming.take(i)) && snd(incoming.drop(i + 1))
}
)
client(someItemPath) // "item/123abc"
server(someItemPath)("item/123abc") // true
server(someItemPath)("foo/bar") // false
Path
constructors is called a fold algebrasealed trait Path
case class Segment(value: String) extends Path
case class Chained(first: Path, second: Path) extends Path
def fold[A](s: String => A, c: (A, A) => A): Path => A = …
type PathAlg[A] = (String => A, (A, A) => A)
def fold[A](alg: PathAlg[A]): Path => A = …
PathAlg
is the fold algebra of Path
trait PathAlg[A] {
def segment(value: String): A
def chained(fst: A, snd: A): A
}
def fold[A](alg: PathAlg[A]): Path => A = …
PathAlg
is our original object algebra interface!Path
type does not even need to exist!val someItemPath: Path =
Chained(Segment("item"), Segment("123abc"))
def someItemPath[A](alg: PathAlg[A]): A = {
import alg._
chained(segment("item"), segment("123abc"))
}
sealed trait PathF[A]
case class ConstSegment(value: String) extends PathF[Unit]
case object StringSegment extends PathF[String]
case object IntegerSegment extends PathF[Int]
case class Chained[A, B](fst: Path[A], snd: Path[B]) extends PathF[(A, B)]
type Path[A] = Free[PathF, A]
Path[A]
is a monad (though we did not implement Monad[PathF]
) that will be bound to an actual monadic context at interpretation time.Path[A]
constructorsdef constSegment(value: String): Path[Unit] =
liftF(ConstSegment(value))
val stringSegment: Path[String] =
liftF(StringSegment)
val integerSegment: Path[Int] =
liftF(IntegerSegment)
def chained[A, B](fst: Path[A], snd: Path[B]): Path[(A, B)] =
liftF(Chained(fst, snd))
type PathRouting[A] = String => Option[A]
Monad
;val pathRoutingMonad: Monad[PathRouting] =
new Monad[PathRouting] {
def pure[A](a: A): PathRouting[A] = _ => Some(a)
def flatMap[A, B](
path: PathRouting[A],
f: A => PathRouting[B]
): PathRouting[B] =
incoming => path(incoming).flatMap(f)(incoming)
}
PathF
to the interpreter’s type:object Routing extends NaturalTransformation[PathF, PathRouting] {
def apply[A](path: PathF[A]): PathRouting[A] =
path match {
case ConstSegment(value) =>
incoming => if (URLDecoder.decode(incoming) == value) Some(()) else None
case StringSegment =>
incoming => Some(URLDecoder.decode(incoming))
case IntegerSegment =>
incoming => Try(incoming.toInt).toOption
case Chained(fst, snd) =>
incoming => {
val i = incoming.indexOf("/")
if (i >= 0) {
fst.foldMap(Routing).apply(incoming.take(i))
.zip(snd.foldMap(Routing).apply(incoming.drop(i + 1)))
.headOption
} else None
}
}
}
// {name}/{id}
val item = chained(stringSegment, integerSegment)
foldMap
:item.foldMap(Routing).apply("foo/42") // Some(("foo", 42))
item.foldMap(Routing).apply("foo/bar") // None
sealed trait PathF[A]
…
type Path[A] = Free[PathF, A]
sealed trait QueryStringF[A]
…
type QueryString[A] = Free[QueryStringF, A]
type PathAndQS[A] = Free[???, A]
PathAndQS
out of Path
and QueryString
Free[F[_], _]
data type until this F[_]
is fully known ;type PathAndQS[A] = PathF :|: QueryStringF :|: FXNil
Free[PathAndQS, A]
:// {name}?{id=…}
val item =
for {
name <- StringSegment.freek[PathAndQS]
id <- QueryStringParameter("id").freek[PathAndQS]
} yield (name, id)
type PathAndQSRouting[A] = Request => Option[A]
object PathRouting extends (PathF ~> PathAndQSRouting) { … }
object QSRouting extends (QueryStringF ~> PathAndQSRouting) { … }
val PathAndQSRouting = PathRouting :&: QSRouting
type PathClient[A] = A => String
type PathDocumentation[A] = String
FreeApplicative
similar to Free
;Free
or FreeApplicative
).Free[F[_], _]
type,FreeApplicative
, for instance, which would require the interpreter to be only an applicative functor,Free
and FreeApplicative
Example in Scala:
val readItem: Endpoint[String, Item] =
endpoint(
get(path / "items" / segment[String]),
jsonResponse[Item]
)
// e.g. "/fr/index", "/en/index", etc.
val index =
endpoint(get(path / segment[Lang] / "index"), htmlResponse)
// e.g. "/fr/about/", "/en/about", etc.
val about =
endpoint(get(path / segment[Lang] / "about"), htmlResponse)
def i18nPage(title: String): Endpoint[Lang, Html] =
endpoint(get(path / segment[Lang] / title), htmlResponse)
val index = i18nPage("index")
val about = i18nPage("about")
endpoint(
get(path / "asset" / assetSegments),
if (gzipEnabled) gzippedAssetResponse else assetResponse
)