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
Generalizando funções comuns
No post anterior sobre ferramentas do Reader Monad , apresentei alguns códigos para ajudar a lidar com operações comuns com o Reader Monad . Por favor, leia os posts anteriores, pois vou pular a longa introdução neste artigo e ir para uma escala bastante complexa.
Apenas uma breve introdução Reader
, é um wrapper, um pouco como Option
ou Future
, que define uma dependência de um recurso (por exemplo, uma fonte de dados), antes de retornar um resultado. Pois instande readUser(id: Long): Reader[UserConnection, User]
é uma função que irá ler um usuário de um banco de dados, dado a UserConnection
e retornar a User
. Como UserConnection
não é uma conexão concreta, cabe ao chamador da função fornecer uma implementação. A boa notícia Reader
é que ele fornece os métodos map
e flatMap
, para que você possa compor com outros leitores antes de fornecer uma implementação concreta.
Aqui estão duas das funções que propus para simplificar o uso de leitores:
def sequence[C, R](list: Traversable[Reader[C, R]]): Reader[C,Traversable[R]] = reader { conn =>
for { r <- list } yield r(conn)
}
implicit def moveFuture[A, B](future: Future[Reader[A, B]])(implicit context: ExecutionContext): Reader[A, Future[B]] = (conn: A) => {
for (r <- future) yield r(conn)
}
sequence
leva um atravessável (um conjunto) deReader
s, e move-se a recolha noReader
:Traversable[Reader[_, R]] => Reader[_, Traversable[R]]
.moveFuture
pega o futuro de um leitor e move o futuro dentro do leitor:Future[Reader[_, R]] => Reader[_, Future[R]]
Se você olhar o código e a semântica das funções, eles basicamente fazem a mesma coisa. O código real é exatamente o mesmo; isso ocorre porque ambos Future
e Traversable
têm uma map
função para transformar os valores que estão envolvendo.
E se pudéssemos abstrair isso e ter apenas uma função para mover coisas que têm uma map
função em torno do Reader
? Isso seria bom, mas Traversable
e Future
não tem um ancestral comum que preste map
. É aqui que as typeclasses são usadas, então vamos ver como funciona.
A classe CanMap
Há uma boa explicação sobre o que são typeclasses em marakana , recomendo que você a assista antes de continuar lendo se não souber o que é uma Typeclass.
Uma typeclass em scala permite adicionar recursos às classes que você não pode modificar. Imagine o problema descrito anteriormente: queremos dizer ao compilador que Future e Traversable seguem a mesma interface que fornecem map
e flatMap
. No entanto, não podemos mudar as bibliotecas scala para adicionar uma interface comum a esses dois tipos, e há muitos outros tipos potenciais que também fornecem funções que nem sempre podemos modificar ( Option
por exemplo).
Vamos primeiro definir uma característica com o que queremos:
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]
}
CanMap
descreve o comportamento de um tipo M
que contém algum valor (Option, Future, Seq, …) e no qual podemos aplicar o método map
e flatMap
. Isso é basicamente o que uma typeclass é, ela define o comportamento geral de um conjunto de tipos.
Vamos ver como podemos definir esse comportamento para a Option
classe:
class OptCan[A, B] extends 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)
}
Essa é uma implementação muito simples, pois ela apenas faz o proxy das funções map e flatMap para as implementações reais de Option. Existem typeclasses mais complexas que criariam métodos totalmente novos (como exportar para Json) para um conjunto completo de tipos.
Mas, como usamos essa typeclass? Vamos reescrever o sequence
método para usá-lo:
def sequence[C, R, D[_]](list: D[Reader[C, R]])(implicit canMap: CanMap[Reader[C, R], R, D]): Reader[C, D[R]] = reader { conn =>
canMap.map(list) { r: Reader[C, R] =>
r(conn)
}
}
Parece um pouco diferente da nossa primeira implementação. Para começar, além dos parâmetros de tipo do leitor ( C
a dependência e R
o tipo de resultado), é necessário um terceiro parâmetro D
que é o tipo genérico do nosso wrapper (que pode ser uma Opção ou um Futuro, etc.).
A parte interessante é como dizemos ao scala que esse tipo D
deve seguir o traço CanMap
.
Como eu disse anteriormente, não podemos acrescentar CanMap
como uma superclasse de Option
, por isso, o que fazemos, é para dizer ao compilador para olhar no contexto de uma implementação do CanMap
que é aplicável a este tipo: .(implicit canMap: CanMap[Reader[C, R], R, D])
Para que o scala encontre isso no contexto implícito, precisamos alterar ligeiramente nossa declaração anterior de OptCan para ser implícita:
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)
}
Agora podemos escrever uma chamada para a função de sequência:
val read: Option[Reader[Int, Int]] = Some(reader { c: Int => c*2 })
val move: Reader[Int, Option[Int]] = Reader.sequence(read)
Não há necessidade de passar o implícito, CanMap
pois o compilador o encontrará automaticamente com base nos tipos adjacentes da chamada de função. Como usamos um , o scala sabe que deve procurar uma implementação que é fornecida por nossa definição implícita.Option[Reader[Int, Int]]
CanMap[Int, Int, Option]
Se quisermos que a função de sequência funcione Future
, a única coisa que precisamos saber é adicionar uma nova implementação implícita de CanMap
:
implicit def canmapfuture[A, B](implicit ex: ExecutionContext) = new CanMap[A, B, Future] {
def map(l: Future[A])(f: A => B): Future[B] = l.map(f)
def flatMap(l: Future[A])(f: A => Future[B]): Future[B] = l.flatMap(f)
}
Este é novamente bastante trivial, exceto que precisamos ter no escopo um implícito ExecutionContext
para que scala saiba onde os cálculos futuros estão sendo agrupados.
Agora podemos usar o mesmo tipo de chamada para mover um futuro:
val read: Future[Reader[Int, Int]] = Future.successful(reader { c: Int => c*2 })
val move: Reader[Int, Future[Int]] = Reader.sequence(read)
Isso é bastante neet, a sequence
função agora é genérica e pode ser aplicada a qualquer tipo para o qual possamos implementar uma CanMap
typeclass. Como você pode ver, CanMap
é uma maneira de descrever o comportamento de uma classe sem mexer com sua hierarquia de tipo, é uma ferramenta muito poderosa para estender as bibliotecas existentes ou para envolver as bibliotecas Java em uma sintaxe de scala agradável.
Você pode pensar que este é um monte de código de scafolding para uma função tão simples, mas como veremos no próximo post, é uma typeclass muito útil e realmente torna o código do Reader mais reutilizável e faz o uso de Readers no resto do seu limpador de código.
Traversable e CanBuildFrom
Mas e quanto à implementação CanMap
para Traversable
? Isso é um pouco mais complexo devido à estrutura da implementação da coleção nas bibliotecas scala. Desde o scala 2.8, a API de coleta é estruturada de uma maneira genérica muito (inteligente), o que é bastante complexo de entender no início. No entanto, depois que você entende o que são typeclasses, não é tão complicado.
Para construir o CanMap
for Traversable
s, primeiro precisamos entender o que CanBuildFrom
faz. Se olharmos para a assinatura (real) de map
e flatMap
no Traversable
traço:
def flatMap[B, That](f: (A) ⇒ GenTraversableOnce[B])(implicit bf: CanBuildFrom[Traversable[A], B, That]): That
def map[B, That](f: (A) ⇒ B)(implicit bf: CanBuildFrom[Traversable[A], B, That]): That
Não é muito simples, existem duas coisas estranhas:
- um tipo
That
, que é o tipo da nova coleção retornada pelo mapa - um implícito
CanBuildFrom
Se você entendeu o princípio das typeclasses, terá identificado isso implícito CanBuildFrom
como uma typeclass. Na verdade, esta typeclass é uma forma de definir métodos para construir novos travessíveis, sem ter que definir todos os travessíveis possíveis que podem ser construídos a priori.
Mas por que você precisa disso?
Imagine uma assinatura ingênua de map
em Traversable:
def map[B](f: A => B): Traversable[B]
Isso sempre retornará um tipo de Traversable quando você chamar map em uma subclasse dessa característica. Isso não é muito prático, imagine que você tenha um Set
, ao aplicar o mapa, você gostaria que ele retornasse a Set
, e não a Traversable
. Você pode escrever um mapa personalizado dentro de cada coleção que retorna o mesmo tipo de sua coleção. Mas imagine um conjunto especializado como BitSet
, que é otimizado para armazenar números inteiros não negativos. Se você ligou , você não pode retornar um como faria com uma coleção de .myBitSet.map((a: Int) => "elt: "+i)
BitSet
String
CanBuildFrom
está lá para ajudar, ele permite que o scala escolha o melhor tipo possível da coleção retornada, com base na implementação disponível dessa typeclass que está atualmente no escopo. em seguida, explica ao scala como construir uma nova coleção de tipo ( por exemplo) a partir da travessia atual.CanBuildFrom[Traversable[A], B, That]
That
Set[String]
Então, em nossa implementação de CanMap
for Traversable
, temos que manter esse recurso e nos certificar de que temos o direito CanBuildFrom
no escopo de nossa typeclass.
Aqui está como isso é feito (graças a uma resposta stackoverflow ):
implicit def canmaptrav[A, B, M[+_]](implicit bf: CanBuildFrom[M[A], B, M[B]], ev: M[A] => TraversableLike[A, M[A]], eb: M[B] => TraversableLike[B, M[B]]) = new CanMap[A, B, M] {
def map(l: M[A])(f: (A) => B): M[B] = l.map(f)
def flatMap(l: M[A])(f: A => M[B]): M[B] = l.flatMap[B, M[B]] { (a: A) => f(a) }
}
As implementações de map
e flatMap
são praticamente idênticas às de Option
ou Future
. A diferença é que colocamos três implícitos no contexto:
CanBuildFrom[M[A], B, M[B]]
diz ao scala como construir coleções contendoB
uma coleção do tipoA
. Isso é necessário para amap
função construir a nova coleção com o tipo alterado.M[B] => TraversableLike[…]
são conversões implícitas para converter nosso tipo de contêinerM
emTraversableLike
tipos válidos . Esta é uma maneira de dizer ao scala que estamos trabalhando apenas em tipos que podem ser convertidos em travessíveis, mesmo se eles não estiverem dentro da hierarquia de tipos percorríveis para começar.
Está tudo feito, agora podemos fazer uma chamada para sequence
da seguinte maneira:
val reader: Seq[Reader[Int, Int]] = Seq(reader { c: Int => 10 }, reader { c: Int => 20 })
Reader.sequence(reader)
Uma Nota Final
Você pode encontrar a implementação de CanMap e Reader na essência a seguir .
Se você está acostumado com programação funcional, deve ter notado que nossa typeclass CanMap
descreve algo muito semelhante a uma Mônada . Na verdade, scalaz fornece uma implementação mais completa deste typeclass, e se você ousar, você pode ir olhar nos Functor
e Monad
typeclasses fornecidos por esta biblioteca.
No próximo post, mostrarei como podemos continuar generarilizando o ferramental fornecido no post anterior usando esta CanMap
typeclass.