Using Object Algebras To Design Domain Specific Embedded Languages

Julien Richard-Foy

Curry On – July 19th, 2016


http://julienrf.github.io/curry-on-2016

Outline

1 Motivation

1.1 Who am I?

Software engineer

1.2 Web engineering challenges

challenges

challenges

Example: e-shop (server)

val readItem = Router.from {
  case GET(p"/item/$id") =>
    itemsRepository.lookup(id)
      .map(item => Ok(representation(item)))
}

Example: e-shop (client)

def eventualItem(id: String): Future[Item] =
  httpClient
    .url(s"http://eshop.com/item/${urlEncode(id)}")
    .get()
    .map(response => decodeItemFromJsonRepresentation(response))

Example: e-shop (documentation)

$ curl http://eshop.com/item
{
  "data": [
    {
      "href": "http://eshop.com/item/123abc",
      "format": {
        "name": "string",
        "price": "decimal"
      }
    }
  ]
}

1.3 Motivation: Summary

What we want

Engineering concern: extensibility

Engineering concern: language modularization

2 Object Algebras

2.1 References

References

2.2 Object Algebras In A Nutshell

First, specify the DSL domain

An algebra interface for URL paths

trait PathAlg[A] {

  def segment(value: String): A

  def chained(first: A, second: A): A

}

Usage

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"))
}

Client interpreter

object PathClient extends PathAlg[String] {

  def segment(value: String): String = URLEncoder.encode(value)

  def chained(first: String, second: String): String = s"$first/$second"

}
someItemPath(PathClient) // "item/123abc"

Server interpreter

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

Summary

2.3 Variations In Scala

Type members

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
}

Type members (2)

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
}

Type members (3)

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"))
}

Syntax

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("")
}

Syntax (2)

// defines the URL path “/item/123abc”
def someItemPath(alg: PathAlg): alg.Path = {
  import alg._
  path / "item" / "123abc"
}

Type constructors as carrier types

Type constructors as carrier types (2)

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)]

}

Type constructors as carrier types (3)

// “{name}/{id}”
def item(alg: PathAlg): alg.Path[(String, Int)] = {
  import alg._
  stringSegment / integerSegment
}

Type constructors as carrier types (4)

object PathClient extends PathAlg {

  type Path[A] = A => String

  …
}
item(PathClient)(("foo", 42)) // "foo/42"

Type constructors as carrier types (5)

object PathRouting extends PathAlg {

  type Path[A] = String => Option[A]

  …
}
item(PathRouting)("foo/42")  // Some(("foo", 42))
item(PathRouting)("foo/bar") // None

3 Notable features

3.1 Extensibility

Retroactive addition of new vocabulary

trait EndpointAlg extends UrlAlg {
  type Request
  def get(url: Url): Request
}
val someItemRequest =
  get(url(path / "item" / "123abc"))

Retroactive addition of new interpreters

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) = …

}

Languages and interpreters can be combined by mixing traits

mixins

mixins

3.2 Modular Expressive Power

Remember the variant of PathAlg that uses a type constructor as a carrier type

trait 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)]

}

Let’s make the carrier type an applicative functor

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))

}

Let’s make the carrier type a monad

trait PathMonadAlg extends PathApplicativeAlg {

  val pathMonad: Monad[Path]

  import pathMonad._

  lazy val pathApplicative = pathMonad

}

Let’s revisit the PathRouting interpreter

trait 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) }
    }

}

Let’s revisit the PathDocumentation interpreter

trait 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}"
}

At use site, pick the expressive power you need

def program1(alg: PathApplicativeAlg) = …
def program2(alg: PathMonadAlg) = …

3.3 DSL Families

We can encode subtyping relationships between carrier types

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
}

DSL families are DSLs that specialize other DSLs

trait UrlAlg {

  type Path <: Url
  def segment(value: String): Path
  def chained(first: Path, second: Path): Path
  …
  type Url
  …
}

4 Putting things together

Remember the motivating example

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))

Let’s define the request once, using our DSL

def readItemRequest(alg: EndpointAlg): alg.Request[String] = {
  import alg._
  get(path / "item" / stringSegment)
}

Revisited server part

Before:

val readItem = Router.from {
  case GET(p"/item/$id") =>
    lookupAndRenderItem(id)
}

After:

val readItem = Router.from {
  readItemRequest(EndpointRouting).returning { id =>
    lookupAndRenderItem(id)
  }
}

Revisited client part

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))

Applying a documenting interpreter

readItemRequest(EndpointDocumentation) // "GET  /item/{string}"

5 Comparison With Free Monads

Free monads abstract over the monadic context of a DSL interpreter

  1. Define your language as a data type ;
  2. Wrap the data type constructors into the free monad ;
  3. Users can write programs using them ;
    • They can benefit from the expressive power of a monad,
  4. Implement an interpreter, which eventually is a monad.

Object algebras vs free monads

6 Conclusion

Object algebras

7 Questions?

8 Bonus

8.1 Practicality

Alternative way of using an algebra interface

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])
}

Alternative way of using an algebra interface (2)

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

8.2 Performances

Performance overhead of library-level abstractions

An interpreter that generates code

Program transformations applying rewrite rules

8.3 Relation With Fold Algebras

Fold algebras?

sealed trait Path
case class Segment(value: String) extends Path
case class Chained(first: Path, second: Path) extends Path

We can then construct paths in a similar way than previously

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"))

And we can define interpreters as folds

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))
    }
  )

Example of use

client(someItemPath) // "item/123abc"
server(someItemPath)("item/123abc") // true
server(someItemPath)("foo/bar") // false

The product of functions matching the Path constructors is called a fold algebra

sealed 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 = …

From fold algebras to object algebras

type PathAlg[A] = (String => A, (A, A) => A)
def fold[A](alg: PathAlg[A]): Path => A = …
trait PathAlg[A] {
  def segment(value: String): A
  def chained(fst: A, snd: A): A
}
def fold[A](alg: PathAlg[A]): Path => A = …

By using Church encoded values, the 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"))
}

Relation With Fold Algebras

8.4 (Deeper) comparison with free monads

Free monads abstract over the monadic context of a DSL interpreter

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]

Actual Path[A] constructors

def 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))

Server interpreter

type PathRouting[A] = String => Option[A]
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)
  }

Server interpreter (2)

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
        }
    }
}

Usage

// {name}/{id}
val item = chained(stringSegment, integerSegment)
item.foldMap(Routing).apply("foo/42")  // Some(("foo", 42))
item.foldMap(Routing).apply("foo/bar") // None

DSLs combination

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]

DSLs combination (2)

type PathAndQS[A] = PathF :|: QueryStringF :|: FXNil
// {name}?{id=…}
val item =
  for {
    name <- StringSegment.freek[PathAndQS]
    id <- QueryStringParameter("id").freek[PathAndQS]
  } yield (name, id)

DSLs combination (3)

type PathAndQSRouting[A] = Request => Option[A]
object PathRouting extends (PathF ~> PathAndQSRouting) { … }
object QSRouting extends (QueryStringF ~> PathAndQSRouting) { … }
val PathAndQSRouting = PathRouting :&: QSRouting

Client interpreter

type PathClient[A] = A => String

Documentation interpreter

type PathDocumentation[A] = String

Object algebras vs free monads

Complexity

Verbosity

Object algebras vs free monads (2)

Extensibility

DSL families

Object algebras vs free monads (3)

Practicality

Modular expressive power

Object algebras vs free monads (4)

Stack-safety

8.5 Embedded DSLs?

Embedding a DSL

Example in Scala:

val readItem: Endpoint[String, Item] =
  endpoint(
    get(path / "items" / segment[String]),
    jsonResponse[Item]
  )

Embeddedding a DSL (2)

// 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")

Embeddedding a DSL (3)

endpoint(
  get(path / "asset" / assetSegments),
  if (gzipEnabled) gzippedAssetResponse else assetResponse
)

Embedded DSL vs “external” DSL