Decrel is a library for declarative programming using relations between your data.
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.
Please visit the documentation for more details.
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]
}
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.
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.
- In this case, all books will have the
Any method that requires an implicit (given) instance of Proof
needs to be called against a val
value.
See this commit for examples.
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.
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
-
You are not required to interact with ZQuery or Fetch datatypes in your application -- simply use the APIs that exposes
ZIO
orF[_]
. ↩ -
Even if your testing library is not supported, adding one is done easily. See
decrel.scalacheck.gen
ordecrel.ziotest.gen
. The implementation code should work for a differentGen
type with minimal changes. ↩