/decrel

Composable relations for Scala

Primary LanguageScalaMozilla Public License 2.0MPL-2.0

Decrel

Continuous Integration Project stage: Experimental Release Artifacts Snapshot Artifacts

Decrel is a library for declarative programming using relations between your data.

Motivation

We commonly use abstractions from optics libraries to zoom into data that is in memory.

For a moment, let's free ourselves of the in-memory limitation, and try imagining an applications' entire datasource as a giant case class.

In such a structure, abstract relations between data will correspond to concrete lenses in optics.

Documentation

Please visit the documentation for more details.

Usecases

For a given domain:

case class Book(id: Book.Id, name: String, author: Author.Id)
object Book {
  case class Id(value: String)
}

case class Author(id: Author.Id, name: String, books: List[Book.Id])
object Author {
  case class Id(value: String)
}

You can declare relations between your entities by extending the appropriate Relation types.

case class Book(id: Book.Id, name: String, author: Author.Id)
object Book {
  case class Id(value: String)
  
  // Define a relation to itself by extending Relation.Self
  // This is useful when composing with other relations later
  case object self extends Relation.Self[Book]
  
  // Define the relation and the kind of relation that exists between two entities
  // Relation.Single means for a book there is a single author
  // depending on your domain, you may want to choose different kinds
  case object author extends Relation.Single[Book, Author]
}

case class Author(id: Author.Id, name: String, books: List[Book.Id])
object Author {
  case class Id(value: String)
  
  case object self extends Relation.Self[Author]

  // Extending Relation.Many means for a given author, there is a list of books
  case object book extends Relation.Many[Author, List, Book]
}

Accessing your data source

To express "given a book, get the author && all the books written by them", looks like this:

val getAuthorAndTheirBooks = Book.author <>: Author.books

But how would you run this with an instance of Book that you have?

val exampleBook = Book(Book.Id("book_id"), "bookname", Author.Id("author_id"))

If your application uses ZIO, there is an integration with ZIO through ZQuery:

import decrel.reify.zquery._
import proofs._  // Datasource implementation defined elsewhere in your code

// Exception is user defined in the datasource implementation
val output: zio.IO[AppError, (Author, List[Book])] = 
  getAuthorAndTheirBooks.toZIO(exampleBook)

Or if you use cats-effect, there is an integration with any effect type that implements cats.effect.Concurrent (including cats.effect.IO) through the Fetch library:

class BookServiceImpl[F[_]](
  // contains your datasource implementations
  proofs: Proofs[F]
) {
  import proofs._

  val output: F[(Author, List[Book])] =
    getAuthorAndTheirBooks.toF(exampleBook) 
}

By default, queries made by decrel will be efficiently batched and deduplicated, thanks to the underlying1 ZQuery or Fetch data types which are based on Haxl.

Generating mock data

You can combine generators defined using scalacheck or zio-test. 2

To express generating an author and a list of books by the author, you can write the following:

val authorAndBooks: Gen[(Author, Book)] =
  gen.author // This is your existing generator for Author
    .expand(Author.self & Author.books) // Give me the generated author,
                                        // additionally list of books for the author

Now you can simply use the composed generator in your test suite.

The benefit of using decrel to compose generators is twofold:

  • less boilerplate compared to specifying generators one-by-one (especially when options/lists are involved)
  • values generated are more consistent compared to generating values independently
    • In this case, all books will have the authorId fields set to the generated author.

Notice to all Scala 3 users

Any method that requires an implicit (given) instance of Proof needs to be called against a val value.

See this commit for examples.

Acknowledgements

Thanks to @ghostdogpr for critical piece of insight regarding the design of the api and the initial feedback.

Thanks to @benrbray for all the helpful discussions.

Thanks to @benetis for pointing out there was a problem that needs fixing.

Thanks to all of my friends and colleagues who provided valuable initial feedback.

License

decrel is copyright Haemin Yoo, and is licensed under Mozilla Public License v2.0

modules/core/src/main/scala/decrel/Zippable.scala is based on https://github.com/zio/zio/blob/v2.0.2/core/shared/src/main/scala/zio/Zippable.scala , licensed under the Apache License v2.0

Footnotes

  1. You are not required to interact with ZQuery or Fetch datatypes in your application -- simply use the APIs that exposes ZIO or F[_].

  2. Even if your testing library is not supported, adding one is done easily. See decrel.scalacheck.gen or decrel.ziotest.gen. The implementation code should work for a different Gen type with minimal changes.