Generalizando o Reader Tooling, parte 2

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

  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

No post sobre Ferramentas do Reader Monad , propus uma série de métodos para simplificar o trabalho com o Reader monad para injeção de dependência. Como mostrei no post anterior , existem maneiras de tornar essas ferramentas mais genéricas. Essa foi uma boa desculpa para explorar typeclasses e mônadas. Por favor, leia os posts anteriores para alguma introdução.

Se você não sabe o que a mônada do Reader faz ou o que é uma typeclass, verifique pelo menos as seguintes postagens:
Curry e Bolo sem indigestão de tipo – Covariância, Contravariância e Mônada do Leitor e Ferramentas da Mônada do Leitor e talvez google para estes conceitos. Existem tutoriais básicos muito bons por aí.

Um caso de uso muito comum com a mônada do Reader é ter um Futuro embutido no Reader, ou, dependendo de como você compõe seus leitores, um Futuro em torno do seu leitor, algo como isto:
“ `scala

val readerF: Reader[Connection, Future[Int]] = 

val readerF
: Future[Reader[Connection, Int]] =

In the previous post, I have shown how to use a typeclass `CanMap` to build a `sequence` function that moves the future inside a Reader, and convert the second case in the first case. While I mention futures, this happens often with collections and with Option. Imagine:

```scala

def getUserIds(): Reader[Connection, Seq[Int]] = …


def readUser(id: Int): Reader[Connection, Option[User]] = …

Se você tentar combinar esses leitores, provavelmente terminará com um que poderá converter em a e, em seguida, usará em a . É por isso que introduzi uma typeclass para lidar com esse padrão de maneira genérica. A typeclass é definida da seguinte forma:Seq[Reader[Connection, Option[User]]]Reader[Connection, Seq[Option[User]]]flattenReader[Connection, Seq[User]]CanMap


trait
CanMap[A, B, M[_]] {
def map(l: M[A])(f: A => B): M[B]
def flatMap(l: M[A])(f: A => M[B]): M[B]
}

Ele define um conjunto de tipos que podem executar a mapou a flatMapno valor que envolvem. Podemos, por exemplo, ter uma implementação para Option:


implicit def canmapopt[A, B] = new CanMap[A, B, Option] {
def map(l: Option[A])(f: A => B): Option[B] = l.map(f)
def flatMap(l: Option[A])(f: A => Option[B]): Option[B] = l.flatMap(f)
}

Para implementações para Traversablee Future, verifique o post anterior ou esta essência . (Você provavelmente deve ter notado que estamos mais ou menos lidando com Mônadas).

É bom poder mover wrappers dentro de um Readerpara torná-lo mais simples de compor ao fazer injeção de dependência, mas você rapidamente acaba tendo que escrever assinaturas de tipo complexas e chamadas mape flatMapem cadeias. Por exemplo, com nosso exemplo anterior, imagine querer obter uma lista de nomes de usuários:


val users
: Reader[Connection, Seq[User]] = getUserIds().flatMap(list =>
Reader.sequence(list.map(readerUser)).map(_.flatten)
)
val userNamesReader
[Connection, Seq[String]] = users.map(list => list.map(user => user.name))

O problema é que “Mônadas não compõem”. No entanto, existe uma solução, podemos apenas fazer um atalho para nossa classe incorporada:


case class ReaderSeq[C, R](r: Reader[C, Seq[R]]) {
def map[B](f: R => B): ReaderSeq[C, B] = ReaderSeq(r.map(list => list.map(f)))
}

Na verdade, no primeiro artigo sobre ferramentas , defini um wrapper muito semelhante para Futures dentro de Readers. Não seria bom se pudéssemos generalizar isso?

Bem, agora que temos essa CanMaptypeclass que generaliza todos os tipos que têm um mape um flatMapmétodo, podemos escrever um wrapper genérico para qualquer classe para a qual temos um CanMap.


case class ReaderM[-C, A, M[+A]](val read: Reader[C, M[A]])(implicit canMap: CanMap[A, _, M]) {
def map[B](f: A => B)(implicit canMap: CanMap[A, B, M]): Reader[C, M[B]] = read.map(in => canMap.map(in)(f))

def flatMap[B, D <: C](f: A => ReaderM[D, B, M])(implicit canMap: CanMap[A, B, M], canMapB: CanMap[B, _, M]): ReaderM[D, B, M] =
ReaderM[D, B, M](read.flatMap { in =>
Reader.reader { (conn: D) =>
canMap
.flatMap(in) { a: A =>
f
(a)(conn)
}
}
})
}

Aqui, definimos um tipo ReaderMque envolve um Reader contendo um tipo M, só aceitamos o tipo Ms que tem uma implementação implícita de CanMapdisponível no escopo. Ou seja, estamos dizendo ao compilador que só aceitamos tipos que tenham uma mape uma flatMapfunção. Dado isso, podemos fazer um mape flatMapque irá proxy do mapa para o tipo dentro do Reader.

Como definimos implementações de CanMapOption, Future e qualquer Traversable, podemos aplicar isso a qualquer um desses casos. Nosso exemplo anterior pode se tornar:


val userNamesReaderM
[Connection, String, Seq] = ReaderM(users).map(user => user.name)

Nota Final

Você pode encontrar o código, com ferramentas estendidas para o Reader e CanMap nesta essência e para o ReaderM nesta essência .

Se você já é um codificador funcional avançado, deve ter notado que CanMapé uma simplificação de um Functor / Monad e que o ReaderM é um Transformador Monad. Você pode encontrar a implementação estendida deles na biblioteca scalaz , mas a implementação fornecida aqui já está funcionando totalmente e usada no código de produção.