7

The Reader Monad is a Monad

 3 years ago
source link: http://m50d.github.io/2016/04/14/the-reader-monad
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

The Reader Monad is a Monad

This article about dependency injection is concerned about the reader monad compromising separation of interface and implementation. In fact, this simply reveals the need to use Scala's most powerful capability: higher-kinded types.

trait BooksService[F[_]] {
    def get(id: Int): F[Book]
}
class CacheingBooksService extends BooksService[Reader[CacheApi, ?]] {
    def get(id: Int): Reader[CacheApi, Book] = ...
}

This is a general pattern: there are many other "effects" that we might want to keep track of in different implementations of an interface:

class RemotedBooksService extends BooksService[Future] {
    def get(id: Int): Future[Book] = ...
}
class TreelogBookService extends BooksService[DescribedComputation] {
    def get(id: Int): DescribedComputation[Book] = ...
}
class DoobieBookService extends BooksService[ConnectionIO] {
    def get(id: Int): ConnectionIO[Book] = ...
}
class AuthorizedDoobieBookService extends BooksService[EitherT[ConnectionIO, Unauthorized, ?]] {
    def get(id: Int): EitherT[ConnectionIO, Unauthorized, Book] = ...
}

We can use all these implementations and more with the same client code, because all these things are monads:

class AggregateBooksService[F[_]: Monad](booksService: BooksService[F], authorService: AuthorService[F])
    def fetchAllBooksBySameAuthorAs(bookId: Int): F[Vector[Book]] =
        for {
            book <- booksService.get(bookId)
            author <- authorService.get(book.authorId)
            books <- author.books.traverse(booksService.get)
        } yield books

What if booksService and authorService use different effect stacks? No problem, we can use MonadPartialOrder to handle both:

class AggregateBooksService2[F[_]: Monad, G[_], H[_]](booksService: BooksService[G], 
authorService: AuthorService[H])(implicit fg: MonadPartialOrder[F, G], fh: MonadPartialOrder[F, G]) extends IAggregateBooksService[F] {
    def fetchAllBooksBySameAuthorAs(bookId: Int): F[Vector[Book]] =
        for {
            book <- fg(booksService.get(bookId))
            author <- fh(authorService.get(book.authorId))
            books <- fg(author.books.traverse(booksService.get))
        } yield books

The end result is that we make testing very easy: our unit tests can follow the exact same code paths as live code, but without having to worry about any effects.

class AggregateBooksService2Test {
    val stubBooksService: BooksService[Id] = ...
    val stubAuthorService: AuthorService[Id] = ...
    val aggregateBooksService2 = new AggregateBooksService2[Id, Id, Id](stubBooksService, stubAuthorService)
    ...
}

Our effects - dependencies via Reader, but also things like remoting, audit logging, database access or possible failures - are minimal overhead in day-to-day code (just the difference between = and <-). But they're not entirely invisible: we can look at the type of a function and see exactly what kind of effects it might have or depend on. If we're using the Reader monad, that means we can see exactly what dependencies any call has, just by mousing over it in our IDE - which in turn means we can easily see when something's not quite right ("why does logging in need the BookService?"). The end result is code that's very concise, but without any "magic" or reflection: everything's plain old code, which is the easiest kind to maintain.

Home


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK