Ferramentas do Reader Monad

Este post é parte de uma série em que construo incrementalmente a ferramenta de injeção de dependência do Reader, usando diferentes conceitos avançados de programação funcional:

  1. Injeção de dependência para configurar conexão (ões) de banco de dados do Play Framework, parte 1
  2. Injeção de dependência para configurar conexão (ões) de banco de dados do Play Framework, parte 2
  3. Caril e Bolo sem indigestão de tipo – Covariância, Contravariância e a Mônada do Leitor
  4. Ferramentas do Reader Monad
  5. Generalizando o Reader Tooling, parte 1
  6. Generalizando o Reader Tooling, parte 2

Nas postagens anteriores , desenvolvi uma implementação funcional do Reader Monad para simplificar a injeção de dependência no Scala.

Este é um padrão muito útil e eu recomendo que você leia os posts anteriores para entendê-lo, caso ainda não saiba. Se você já sabe do que se trata e só quer uma implementação, veja no final do post anterior.

Nesta postagem, fornecerei algumas funções úteis para compor e trabalhar com a mônada do Reader que simplificam alguns padrões comuns de uso.

Leitores de redação

Freqüentemente, você acaba com dois leitores separados, vindos de dependências diferentes (por exemplo, conexões diferentes). A primeira função a ajudar é a zipque pegará dois leitores e retornará um único leitor com os resultados dos dois leitores compostos em uma tupla:


case class Reader[-C, +A](run: C => A) {

def zip[B, D <: C](other: Reader[D, B]): Reader[D, (A, B)] =
this.flatMap { a =>
other
.map { b => (a, b) }
}
}

Você pode usá-lo da seguinte maneira:


val reader1
: Reader[UserConnection, User] =
val reader2
: Reader[PostConnection, Seq[Post]] =

reader1
.zip(reader2).map {
case (user, posts) => ...
}

Outro caso de uso comum onde você tem muitos leitores separados que você pode querer compor é quando você lida com coleções de itens:


val readerList
: Seq[Reader[UserConnection, User]] = Seq(1,2,3).map(id => readUser(id))

Você realmente não quer uma coleção de leitores, mas um leitor de uma coleção. Isso pode ser facilmente resolvido:


def sequence[C, R](list: TraversableOnce[Reader[C, R]]): Reader[C,TraversableOnce[R]] = reader { conn =>
for { r <- list } yield r(conn)
}

e agora você pode fazer:


val list
: reReader[UserConnection, Seq[User]] = sequence(readerList)

Leitor e Futuro

Conforme explicado nas postagens anteriores , a mônada do Reader é boa para abstrair suas fontes de dados. Na maioria dos casos, se você tiver acesso a um banco de dados ou a um serviço da web que fornece os dados, sua conexão retornará o resultado final em um Futureinvólucro sem bloqueio . Se você também usar Readers, terá uma estranha mistura das duas mônadas.

A primeira questão que surge é que, ao combinar Futuros com devolução de leitores, você acaba tendo um tipo de retorno igual ou igual . Isso ocorre porque as duas mônadas não se compõem automaticamente. Da mesma forma que você não quer uma coleção de Leitores, é melhor ter um Leitor de um Futuro que você possa compor com outros Leitores do que um monte de Futuros de Leitores que são complicados de compor com Leitores de valores não futuros.Future[Reader[A, B]]Future[Reader[A, Future[B]]]

Usando uma conversão implícita, podemos mover o leitor e simplificar nosso código com apenas duas declarações:


implicit def moveFuture[A, B](future: Future[Reader[A, B]])(implicit context: ExecutionContext): Reader[A, Future[B]] = (conn: A) => {
for (reader <- future) yield reader(conn)
}

implicit def moveFutureFuture[A, B](future: Future[Reader[A, Future[B]]])(implicit context: ExecutionContext): Reader[A, Future[B]] = {
val future1
= moveFuture(future)
future1
.map(f => f.flatMap(inf => inf))
}

As assinaturas de função são um pouco complexas, mas a única coisa que fazem é desembrulhar o leitor no futuro com uma dependência fornecida fora do futuro ( conn). A segunda função é apenas um atalho para nivelar futuros consecutivos.

Outra questão básica que surge é a criação de um valor “puro” para um futuro dentro de um leitor. Ou seja, um resultado já calculado, sem ser adiado ou sem que haja injeção de dependências. Podemos escrever de forma bastante simples:


def pure[A, B](value: B) = Reader.pure[A, Future[B]](Future.successful(value))

Uma vez que temos um futuro dentro de um leitor, o problema principal não é conseguir código legível. Freqüentemente, você não quer trabalhar no Reader ou no Futuro, mas no valor contido nos dois. Ou seja, você deseja usar um map. Então você acaba fazendo algo semelhante a:


val reader
: Reader[A, Future[String]] =
val parsed
: Reader[A, Future[Int]] = reader.map(_.map(_.toInt))

Em breve, seu código estará cheio de e . Que são muito feios e tornam o código ilegível sem nenhum motivo particular. Podemos simplificar isso declarando funções apenas para o caso de leitores que contêm futuros:flatMap(_.map(…))map(_.map(…))


implicit class ReaderFuture[-C, +A](val reader: Reader[C, Future[A]]) {
/**
* shortcut for flatMap{ _.map {...} } when the inner block returns a normal result

*/

def flatMapMap[B, D <: C](f: A => Reader[D, B])(implicit context: ExecutionContext): Reader[D, Future[B]] = reader.flatMap { future => future.map(f) }

/**
* shortcut for map(_.map(...))

*/

def mapMap[B](f: A => B)(implicit context: ExecutionContext): Reader[C, Future[B]] = reader.map { future => future.map(f) }

/**
* shortcut for flatMap{ _.map {...} } when the inner block returns a Reader[_, Future[_]] (we also move the future around

*/

def flatMapMapF[B, D <: C](f: A => Reader[D, Future[B]])(implicit context: ExecutionContext): Reader[D, Future[B]] = reader.flatMap { future => future.map(f) }


}

Usamos classes implícitas do scala 2.10 para definir uma nova função para qualquer um .Reader[C, Future[A]]

  • flatMapMapé chamado quando você tem um mapem um futuro dentro de um flatMapem um Reader
  • mapMapsegue o mesmo padrão, mas quando você está em um mapem um Reader

Finalmente, apenas para ajudar o compilador a resolver a conversão implícita e nivelar Futuros consecutivos adequadamente, podemos usar flatMapMapFquando o mapbloco mais interno retornar um futuro. Na verdade, esse é o caso que encontro com mais frequência ao codificar com uma mistura de injeções de dependência.

Você pode encontrar o código completo do Reader com todas essas ferramentas nesta essência .

Observe que o mesmo tipo de implícito pode ser declarado para lidar com Option em Readers, pois isso também acontece com frequência.