Desenvolvimento

31 jan, 2014

Functors contravariantes – uma intuição

Publicidade

Recentemente, me deparei com um artigo listando diferentes tipos de functors (expressos usando Scala) e mais uma vez encontrei uma menção a functors contravariantes. Já ouvi falar deles no passado, vi como funcionavam, mas… não é epifania.

Neste artigo, eu gostaria de ter uma noção sobre o que é um functor contravariante. Deixarei para outro a compreensão sobre outro functor assustador, o exponencial (não deve ser muito difícil, presumo eu, parece ser composto por dois outros functors: um covariante e outro contravariante).

Aqui está a assinatura dada para um functor contravariante no artigo que li recentemente:

trait Contravariant[F[_]] {
  def contramap[A, B](f: B => A): F[A] => F[B]
}

É muito complicado mentalmente (para mim) entender como aquela função f consegue o valor do tipo B quando B não pode ser encontrado na lista de argumentos, mas apenas no tipo de retorno (em oposição a A). O que acontece então?

Um exemplo concreto

Depois de uma breve busca na internet, encontrei que um bom exemplo de um functor contravariante pode na verdade ser visto na biblioteca padrão do Scala. Ordering.by é uma incarnação contramap, especializado para instâncias Ordering.

def by[T, S](f: T => S)(implicit ord: Ordering[S]): Ordering[T]

A assinatura parece um pouco diferente do que aquela usada por Tony Morris, mas são conceitualmente as mesmas (a anterior é só mais geral). O que ela diz é que se você souber como comparar elementos do tipo S e tiver uma função relacionando elementos do tipo T a elementos do tipo S, então você sabe como comparar elementos do tipo T.

Há dois mapeamentos conceituais aqui. Num nível mais alto, você quer mapear Ordering[S] para Ordering[T], mas, para conseguir isso, um mapeamento oposto é necessário – de T para S – portanto “contra”.

Generalizando, temos contramap como uma função que diz que se você der a ela alguma abstração sobre o conceito A, por exemplo, F[A] e uma função que mapeia um conceito B diferente para um conceito A, então é capaz de te retornar uma abstração sobre B (ex. F[B]). Muito abstrato talvez, mas confie em mim.

Intuição

Aqui está um exemplo real inspirado por esta thread.

Todo mundo sabe como comparar números, certo? 2 é menor do que 3, que é maior do que 2. Todo mundo também sabe o que é dinheiro. É um conceito que já temos usado há eras, e é bastante trivial traçar uma correspondência entre dinheiro e números. Mapeamos uma nota de um real (100 centavos) ao número 100, por exemplo.

Por que sabemos essas duas coisas: 1. como comparar números; 2. como mapear dinheiro para números, podemos comparar dinheiro. Óbvio, certo? Bem, chamando contramap usando esses dois fatores como argumentos, forneceremos a você o conhecimento sobre como comparar dinheiro. Esse é exatamente o conhecimento que um functor contravariante codifica, mas de uma maneira geral, não apenas para números, dinheiro ou comparações.

Conforme dito, o objetivo final é, na verdade, mapear F[A] => F[B], mas, para conseguirmos isso, precisamos de uma função que mapeie B => A, no sentido oposto (contra). É basicamente abstração. Você constrói novas abstrações a partir de antigas ao especificar a relação entre os conceitos.

Traduzindo para o Scala

Vamos colocar o exemplo acima em código Scala. Primeiro, vamos representar Money (dinheiro):

case class Money(amount: Int)

Scala já sabe como comparar Ints e, baseado no conhecimento que queremos especificar, como Money deve ser comparado, derivo Ordering[Money] de Ordering [Int]. Para conseguir isso, precisamos de uma forma de especificar qual subcomponente de Money é nossa Int desejada, o que é trivial neste exemplo:

val contramapFn: Money => Int = (money) => money.amount

Tendo definido isso e dado que uma instância implícita de Orderin[Int] já está no escopo, tudo o que precisamos fazer agora é chamar a função contramap definida no objeto Ordering, ou seja, pelo método by. Ordering [Money] resultante é marcado como sendo implícito porque queremos que o compilador use-o quando utilizarmos o método < de comparação.

implicit val moneyOrd: Ordering[Money] = Ordering.by(contramapFn)

Agora, podemos comparar facilmente instâncias de Money (ainda precisamos de uma conversão implícita de Ordering para Ordered no escopo, portanto o primeiro import):

scala> import scala.math.Ordered._
import scala.math.Ordered._

scala> Money(13) < Money(20)
res0: Boolean = true

scala> Money(23) < Money(20)
res1: Boolean = false

Implementação do by

Entretanto, como o Ordering.by é implementado? Uma implementação possível se parece com isto (a original é um pouco mais complicada devido a otimizações):

def by[T, S](f: T => S)(implicit ord: Ordering[S]): Ordering[T] =
  new Ordering[T] {
    def compare(x:T, y:T) = Ordering[S].compare(f(x), f(y))
  }

Como você pode ver, ela cria uma instância Ordering cujo método compare simplesmente delega ao método compare do Ordering[S] conhecido, mas não antes de transformar os parâmetros x e y ao passá-los por f.

Essa implementação simples também dá uma ideia de como f é capaz de receber argumentos do tipo T. Ela cria uma nova “função” que receberá Ts para passar ao f. Quando precisar de um valor de um tipo específico e não tiver, o que fazer? Você retorna uma função que pergunta por um. Nesse caso particular, não temos uma função apropriada, mas um objeto (uma instância de Ordering), que não é nada além do que uma coleção de funções, por isso as razões são essas.

Conclusão

Há mais algumas implementações concretas de functors contravariantes nas páginas linkadas na seção fontes abaixo, mas o princípio permanece o mesmo – você cria novas abstrações a partir de antigas ao fornecer uma função de mapeamento do novo conceito, sendo abstraído da função antiga que já foi abstraída anteriormente.

Fontes

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em  http://igstan.ro/posts/2013-10-31-contravariant-functors-an-intuition.html