Generalizando o Reader Tooling, parte 1

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

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 Optionou 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 UserConnectione retornar a User. Como UserConnectionnã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 mape 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)
}
  • sequenceleva um atravessável (um conjunto) de Readers, e move-se a recolha no Reader: 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 Futuree Traversabletêm uma mapfunçã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 mapfunção em torno do Reader? Isso seria bom, mas Traversablee Futurenã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 mape 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 ( Optionpor 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]
}

CanMapdescreve o comportamento de um tipo Mque contém algum valor (Option, Future, Seq, …) e no qual podemos aplicar o método mape 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 Optionclasse:


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 sequencemé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 ( Ca dependência e Ro tipo de resultado), é necessário um terceiro parâmetro Dque é 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 Ddeve seguir o traço CanMap.
Como eu disse anteriormente, não podemos acrescentar CanMapcomo uma superclasse de Option, por isso, o que fazemos, é para dizer ao compilador para olhar no contexto de uma implementação do CanMapque é 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, CanMappois 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 ExecutionContextpara 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 sequencefunção agora é genérica e pode ser aplicada a qualquer tipo para o qual possamos implementar uma CanMaptypeclass. 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 CanMappara 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 CanMapfor Traversables, primeiro precisamos entender o que CanBuildFromfaz. Se olharmos para a assinatura (real) de mape flatMapno Traversabletraç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 CanBuildFromcomo 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 mapem 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)BitSetString

CanBuildFromestá 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]ThatSet[String]

Então, em nossa implementação de CanMapfor Traversable, temos que manter esse recurso e nos certificar de que temos o direito CanBuildFromno 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 mape flatMapsão praticamente idênticas às de Optionou 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 contendo Buma coleção do tipo A. Isso é necessário para a mapfunçã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êiner M em TraversableLiketipos 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 sequenceda 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 CanMapdescreve 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 Functore Monadtypeclasses fornecidos por esta biblioteca.

No próximo post, mostrarei como podemos continuar generarilizando o ferramental fornecido no post anterior usando esta CanMaptypeclass.