Caril e Bolo sem indigestão de tipo – Covariância, Contravariância e a Mônada do Leitor

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:

  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

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 implicite 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 conncó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]]mapflatMapFutureList

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 mappara 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))

}

Readerleva 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 Totipo de leitor retornado é a Seqe scala está esperando a Iterable. Mesmo se Iterablefor 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[_]]SeqIterable

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]BC

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 userPostsnã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, RealConnectionimplementa 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 PostConnectionUserConnection with PostConnectionRealConnectionReader[RealConnection, _]Reader[UserConnection with PostConnection, _]userPostswithDependency

Em scala, isso é anotado em seu código com -:

case class Reader[-From, +To](wrappedF: From => To)

No entanto, marcar Fromcomo contravariante criará um novo problema. A flatMapfunçã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 flatMapmé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 FromBcomo um subtipo de nosso tipo contravariante.

A Readerclasse 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.