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 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
Em meus dois posts anteriores , falei sobre o uso de diferentes técnicas de injeção para configurar fontes de dados no Play !, embora os exemplos tenham sido baseados nesse framework, eles podem ser usados em qualquer outro projeto scala. Neste post vou continuar no tópico do último post: o uso do Reader Monad e do Cake Pattern, mas sem usar especificidades do Play !.
Leitor monad para injetar fontes de dados (e outros componentes intercambiáveis)
Se você não tem tempo para ler as duas últimas postagens, aqui está um breve resumo, se você acabou de lê-los, pode apenas ler rapidamente para uma atualização.
O problema que estou discutindo é o de configurar fontes de dados, como bancos de dados, sem ter dependências profundas em seu código para uma implementação particular. Imagine, por exemplo, que você tem uma plataforma de blog e que está usando MySQL para armazenar informações sobre seus usuários e suas postagens , então percebe que as postagens são muito estáticas e você não precisa fazer consultas complexas para recuperá-las (ou seja, você apenas precisa listar todas as postagens de um determinado usuário), você pode querer mover esta parte do seu sistema para um banco de dados baseado em documentos, como mongoDB ou couchDB. Se você não foi muito cuidadoso com sua implementação de conexão de armazenamento de dados, você tem consultas mySQL escondidas aqui e ali em seu código, misturando usuáriose Postagens no nível de código e no nível SQL.
A primeira etapa que propus no post anterior foi usar o padrão cake para separar a lógica de conexão da lógica de negócios. Isso envolve a criação de interfaces ( trait
) que representam operações em suas fontes de dados, para cada fonte separável. Em nosso exemplo, esta é uma conexão para a fonte de dados dos usuários e uma para a das postagens:
trait UserConnection {
def readUser(id: Long): Option[User]
….
}
trait PostConnection {
def readPosts(user: User): Seq[Post]
}
Este é um código simplificado, mas essa é a ideia. Então você pode ter uma implementação específica para estes:
class MySQLConnection extends UserConnection { … }
class MongoConnection extends PostConnection { … }
Isso também simplifica o teste, pois você pode implementar simulações de um determinado bit da conexão.
A segunda questão que discuti foi a de injetar a implementação certa nos métodos que estão usando essas conexões. A ideia proposta era evitar implicit
e usar currying. Isso significa que em vez de retornar um resultado ao chamar um método dependendo de uma conexão, você retorna uma função que toma como parâmetro a dependência e, eventualmente, retornará o resultado:
def userPosts(userID: Long): UserConnection with PostConnection => Seq[Post] = conn => {
(conn.readUser(userID) map { user =>
conn.readPosts(user)
}) getOrElse(List()) //getting rid of the Option just to simplify the code in this article
}
Você vai concordar que isso é feio de ler e, quando deseja compor chamadas diferentes, acaba tendo um conn
código oculto que não é simples de seguir e compreender para os novatos. É aqui que entra o Reader Monad ; Eu sei que disse mônada, mas não se preocupe, não é tão ruim assim.
A mônada do Reader é um invólucro em torno de uma função (por exemplo ) que pode ser transformada e composta da mesma forma que você faria para uma coleção semelhante . Se você não tem uma ideia geral do que essas duas funções fazem, verifique a anterior e leia uma introdução sobre elas. Um código ingênuo para a mônada do leitor é fornecido no post anterior e eu o discutirei mais tarde.Connection => Future[Iterable[Post]]
map
flatMap
Future
List
Se você usa com frequência , também pode simplificar a assinatura definindo nossa conexão composta em algum lugar do código:UserConnection with PostConnection
type UserPostConn = UserConnection with PostConnection
então nossa assinatura de função agora se torna:
def userPosts(userID: Long): Reader[UserPostConn, Seq[Post]]
Poderíamos então usar map
para transformar o que está no Reader, por exemplo, para obter apenas os títulos das postagens:
val titlesReader: Reader[UserPostConn, Seq[String]] = userPosts(id).map { postIterable =>
postIterable.map(_.title)
}
Neste exemplo, obtemos o iterável dentro do leitor e extraímos o título de cada item.
Covariância de tipo
No post anterior, dei esta implementação simples de um Reader:
object Reader {
implicit def reader[From, To](f: From => To) = Reader(f)
def pure[From, To](a: To) = Reader((c: From) => a)
}
case class Reader[From, To](wrappedF: From => To) {
def apply(c: From) = wrappedF(c)
def map[Tob](transformF: To => Tob): Reader[From, Tob] =
Reader(c => transformF(wrappedF(c)))
def flatMap[Tob](transformF: To => Reader[From, Tob]): Reader[From, Tob] =
Reader(c => transformF(wrappedF(c))(c))
}
Reader
leva dois parâmetros de tipo, o tipo de dependência From
(a conexão em nosso caso) e o tipo de valor que você obterá assim que resolver a dependência To
.
A primeira questão que você encontrará, mesmo que scala seja muito inteligente sobre isso na maioria dos casos, é fazer um upcast para um leitor com um tipo de retorno mais geral To
. Algo assim:
val posts: Reader[UserPostConn, Iterable[Posts]] = userPosts(10l)
não vai compilar. Isso ocorre porque o To
tipo de leitor retornado é a Seq
e scala está esperando a Iterable
. Mesmo se Iterable
for um ancestral de Seq
, scala não sabe realmente o que fazer a menos que você diga a ele explicitamente.
É para isso que serve a covariância de tipo , permite que você diga que pode ser usado no lugar de como você pode usar no lugar de , porque é inferior na hierarquia de classes. A única coisa que temos que fazer para que o scala deduza isso é anotar o Reader corretamente:Reader[_, Seq[_]]
Reader[_, Iterable[_]]
Seq
Iterable
case class Reader[From, +To](wrappedF: From => To)
O +
informa ao compilador que Reader
é “covariante” para o tipo de parâmetro T
. Para programadores java, é um pouco difícil de entender no início, mas tente o exemplo acima, com e sem a +
anotação. Você pode encontrar o código para isso nesta essência . Basta abrir uma planilha com ele e você verá os resultados.
Contravariância de tipo
Tipos genéricos covariantes fazem sentido rapidamente, parece natural que um tipo que envolve outro tipo siga a mesma hierarquia de tipo que o tipo que ele incorpora. Porém, nem sempre é assim, e existe o tipo inverso de relação: a contravariância.
Uma anotação de tipo contravariante informa ao compilador que um tipo pode ser usado no lugar de se pode ser usado no lugar de . Isso soa totalmente contra-intuitivo e você tem que lê-lo algumas vezes apenas para entender o que significa. A mônada do leitor que estamos estudando nos fornece um bom exemplo de onde a contravariância é necessária.A[B]
A[C]
B
C
Primeiro, vamos começar fazendo uma pequena função de utilidade, que já mencionei no post anterior, para simplificar o uso de um Reader.
Quando você deseja obter o valor de um leitor, você precisa chamar apply nele, passando uma dependência concreta. Algo assim:
val postReader = userPosts(10l)
class RealConnection extends UserConnection with PostConnection
val conn = new RealConnection
val actualPosts = postReader(conn)
Isso é um pouco complicado e nem sempre muito legível para os outros. Podemos definir um pequeno utilitário agradável que cuidará de desembrulhar o leitor:
def withDependency[F, T](dep: F)(reader: Reader[F, T]): T = reader(dep)
Podemos então escrever
val actualPosts = withDependency(new RealConnection) {
…
userPosts(10l)
}
Você acabará tendo um grande bloco de código, onde compõe diferentes leitores juntos, e que eventualmente passa para o withDependency
. Idealmente, em algum lugar no topo do fluxo de dados do aplicativo.
No entanto, se você tentar fazer isso, o compilador emitirá um erro de que o tipo retornado userPosts
não é compatível com o tipo esperado por withDependency
. Na verdade, a função retorna um tempo enquanto esperamos um bloco do tipo .Reader[UserConnection with PostConnection, _]
Reader[RealConnection, _]
No entanto, RealConnection
implementa tudo o que você deseja de um , é um subtipo concreto dessa interface. Assim, você pode usar com eficácia qualquer coisa que seja a quando tiver um . Mas você precisa dizer ao compilador que pode ser usado onde você tem um para que possa fazer o upcast do resultado de para o tipo esperado por .UserConnection with PostConnection
UserConnection with PostConnection
RealConnection
Reader[RealConnection, _]
Reader[UserConnection with PostConnection, _]
userPosts
withDependency
Em scala, isso é anotado em seu código com -
:
case class Reader[-From, +To](wrappedF: From => To)
No entanto, marcar From
como contravariante criará um novo problema. A flatMap
função não será compilada, apresentando o erro:
o tipo contravariante De ocorre na posição covariante no tipo Para => teste.Reader [De, Tob] do valor transformaF
Isso ocorre porque você não pode usar um parâmetro de tipo contravariante como um tipo de resultado, você só pode usá-lo como um tipo de parâmetro de uma função. Esta resposta stackoverflow fornece alguns detalhes de por que isso acontece.
Precisamos tornar o flatMap
método paramétrico neste tipo de retorno para que não seja contravariante:
def flatMap[FromB <: From, ToB](f: To => Reader[FromB, ToB]): Reader[FromB, ToB] =
Reader(c => f(wrappedF(c))(c))
Estamos usando um truque, dizendo que o tipo de resultado é aquele Reader[FromB, ToB]
onde fazemos o escopo FromB
como um subtipo de nosso tipo contravariante.
A Reader
classe agora pode ser usada:
case class Reader[-From, +To](wrappedF: From => To) {
def apply(c: From) = wrappedF(c)
def map[ToB](transformF: To => ToB): Reader[From, ToB] =
Reader(c => transformF(wrappedF(c)))
def flatMap[FromB <: From, ToB](f: To => Reader[FromB, ToB]): Reader[FromB, ToB] =
Reader(c => f(wrappedF(c))(c))
}
Você pode encontrar essa classe e exemplos na essência a seguir , novamente, você pode salvá-la como uma planilha e experimentá-la.