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:
- Injeção de dependência para configurar conexão (ões) de banco de dados do Play Framework, parte 1
- Injeção de dependência para configurar conexão (ões) de banco de dados do Play Framework, parte 2
- Caril e Bolo sem indigestão de tipo – Covariância, Contravariância e a Mônada do Leitor
- Ferramentas do Reader Monad
- Generalizando o Reader Tooling, parte 1
- 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]]]
flatten
Reader[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 map
ou a flatMap
no 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 Traversable
e 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 Reader
para 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 map
e flatMap
em 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 Future
s dentro de Reader
s. Não seria bom se pudéssemos generalizar isso?
Bem, agora que temos essa CanMap
typeclass que generaliza todos os tipos que têm um map
e um flatMap
mé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 ReaderM
que envolve um Reader contendo um tipo M
, só aceitamos o tipo Ms que tem uma implementação implícita de CanMap
disponível no escopo. Ou seja, estamos dizendo ao compilador que só aceitamos tipos que tenham uma map
e uma flatMap
função. Dado isso, podemos fazer um map
e flatMap
que irá proxy do mapa para o tipo dentro do Reader.
Como definimos implementações de CanMap
Option, 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.