Onde Scala procura implícitos?

398

Uma pergunta implícita para os novatos em Scala parece ser: onde o compilador procura implícitos? Quero dizer implícito, porque a pergunta nunca parece totalmente formada, como se não houvesse palavras para ela. :-) Por exemplo, de onde integralvêm os valores abaixo?

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

scala> def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}
foo: [T](t: T)(implicit integral: scala.math.Integral[T])Unit

scala> foo(0)
scala.math.Numeric$IntIsIntegral$@3dbea611

scala> foo(0L)
scala.math.Numeric$LongIsIntegral$@48c610af

Outra pergunta que segue aqueles que decidem aprender a resposta para a primeira pergunta é como o compilador escolhe qual implícito usar, em certas situações de aparente ambiguidade (mas que compila de qualquer maneira)?

Por exemplo, scala.Predefdefine duas conversões de String: uma para WrappedStringe outra para StringOps. As duas classes, no entanto, compartilham muitos métodos; então, por que Scala não se queixa de ambiguidade quando, digamos, chama map?

Nota: esta questão foi inspirada por outra questão , na esperança de expor o problema de uma maneira mais geral. O exemplo foi copiado de lá, porque é referido na resposta.

Daniel C. Sobral
fonte

Respostas:

554

Tipos de implícitos

Implícitos no Scala refere-se a um valor que pode ser passado "automaticamente", por assim dizer, ou a uma conversão de um tipo para outro que é feita automaticamente.

Conversão implícita

Falando brevemente sobre o último tipo, se alguém chama um método mem um objeto ode uma classe C, e essa classe não faz método de apoio m, então Scala vai olhar para uma conversão implícita Cda algo que faz suporte m. Um exemplo simples seria o método mapem String:

"abc".map(_.toInt)

Stringnão suporta o método map, mas StringOpssim, e há uma conversão implícita de Stringpara StringOpsdisponível (consulte a implicit def augmentStringseguir Predef).

Parâmetros implícitos

O outro tipo de implícito é o parâmetro implícito . Eles são passados ​​para chamadas de método como qualquer outro parâmetro, mas o compilador tenta preenchê-las automaticamente. Se não puder, irá reclamar. Um pode passar esses parâmetros explicitamente, que é como se usa breakOut, por exemplo (ver pergunta sobre breakOut, em um dia você está sentindo-se de um desafio).

Nesse caso, é preciso declarar a necessidade de um implícito, como a foodeclaração do método:

def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}

Exibir limites

Há uma situação em que um implícito é uma conversão implícita e um parâmetro implícito. Por exemplo:

def getIndex[T, CC](seq: CC, value: T)(implicit conv: CC => Seq[T]) = seq.indexOf(value)

getIndex("abc", 'a')

O método getIndexpode receber qualquer objeto, desde que haja uma conversão implícita disponível de sua classe para Seq[T]. Por causa disso, posso passar um Stringpara getIndexe ele funcionará.

Nos bastidores, o compilador muda seq.IndexOf(value)para conv(seq).indexOf(value).

Isso é tão útil que existe açúcar sintático para escrevê-los. Usando este açúcar sintático, getIndexpode ser definido assim:

def getIndex[T, CC <% Seq[T]](seq: CC, value: T) = seq.indexOf(value)

Esse açúcar sintático é descrito como um limite de vista , semelhante a um limite superior ( CC <: Seq[Int]) ou um limite inferior ( T >: Null).

Limites de contexto

Outro padrão comum em parâmetros implícitos é o padrão de classe de tipo . Esse padrão permite o fornecimento de interfaces comuns para classes que não as declararam. Pode servir como um padrão de ponte - obtendo uma separação de preocupações - e como um padrão de adaptador.

A Integralclasse que você mencionou é um exemplo clássico de padrão de classe de tipo. Outro exemplo na biblioteca padrão do Scala é Ordering. Há uma biblioteca que faz uso intenso desse padrão, chamado Scalaz.

Este é um exemplo de seu uso:

def sum[T](list: List[T])(implicit integral: Integral[T]): T = {
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

Também há açúcar sintático para ele, chamado de contexto vinculado , que se torna menos útil pela necessidade de se referir ao implícito. Uma conversão direta desse método se parece com isso:

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

Os limites de contexto são mais úteis quando você só precisa passá- los para outros métodos que os utilizam. Por exemplo, o método sortedon Seqprecisa de um implícito Ordering. Para criar um método reverseSort, pode-se escrever:

def reverseSort[T : Ordering](seq: Seq[T]) = seq.sorted.reverse

Como Ordering[T]foi implicitamente passado para reverseSort, ele pode transmiti-lo implicitamente para sorted.

De onde vêm os implícitos?

Quando o compilador vê a necessidade de um implícito, seja porque você está chamando um método que não existe na classe do objeto ou porque você está chamando um método que requer um parâmetro implícito, ele procurará um implícito que atenda à necessidade .

Essa pesquisa obedece a certas regras que definem quais implícitos são visíveis e quais não são. A tabela a seguir, que mostra onde o compilador procurará implícitos, foi tirada de uma excelente apresentação sobre implícitos por Josh Suereth, que eu recomendo vivamente a quem quiser aprimorar seus conhecimentos sobre o Scala. Foi complementado desde então com comentários e atualizações.

Os implícitos disponíveis no número 1 abaixo têm precedência sobre os sob o número 2. Além disso, se houver vários argumentos elegíveis que correspondam ao tipo de parâmetro implícito, um mais específico será escolhido usando as regras da resolução de sobrecarga estática (consulte Scala Especificação §6.26.3). Informações mais detalhadas podem ser encontradas em uma pergunta à qual vinculo no final desta resposta.

  1. Primeiro olhar no escopo atual
    • Implícitos definidos no escopo atual
    • Importações explícitas
    • importações curinga
    • Mesmo escopo em outros arquivos
  2. Agora observe os tipos associados em
    • Objetos complementares de um tipo
    • Escopo implícito do tipo de argumento (2.9.1)
    • Escopo implícito dos argumentos de tipo (2.8.0)
    • Objetos externos para tipos aninhados
    • Outras dimensões

Vamos dar alguns exemplos para eles:

Implícitos definidos no escopo atual

implicit val n: Int = 5
def add(x: Int)(implicit y: Int) = x + y
add(5) // takes n from the current scope

Importações explícitas

import scala.collection.JavaConversions.mapAsScalaMap
def env = System.getenv() // Java map
val term = env("TERM")    // implicit conversion from Java Map to Scala Map

Importações curinga

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

Mesmo escopo em outros arquivos

Edit : Parece que isso não tem uma precedência diferente. Se você tem algum exemplo que demonstra uma distinção de precedência, faça um comentário. Caso contrário, não confie neste.

É como o primeiro exemplo, mas assumindo que a definição implícita esteja em um arquivo diferente do seu uso. Veja também como os objetos de pacote podem ser usados ​​para trazer implícitos.

Objetos complementares de um tipo

Existem dois companheiros de objeto de nota aqui. Primeiro, o objeto complementar do tipo "origem" é examinado. Por exemplo, dentro do objeto Optionhá uma conversão implícita para Iterable, portanto, pode-se chamar Iterablemétodos Optionou passar Optionpara algo que espera um Iterable. Por exemplo:

for {
    x <- List(1, 2, 3)
    y <- Some('x')
} yield (x, y)

Essa expressão é traduzida pelo compilador para

List(1, 2, 3).flatMap(x => Some('x').map(y => (x, y)))

No entanto, List.flatMapespera a TraversableOnce, o que Optionnão é. O compilador, então, procura Optiono objeto complementar do objeto e encontra a conversão para Iterable, que é a TraversableOnce, tornando essa expressão correta.

Segundo, o objeto complementar do tipo esperado:

List(1, 2, 3).sorted

O método sortedleva um implícito Ordering. Nesse caso, ele olha dentro do objeto Ordering, companheiro da classe Ordering, e encontra um implícito Ordering[Int].

Observe que os objetos complementares das super classes também são analisados. Por exemplo:

class A(val n: Int)
object A { 
    implicit def str(a: A) = "A: %d" format a.n
}
class B(val x: Int, y: Int) extends A(y)
val b = new B(5, 2)
val s: String = b  // s == "A: 2"

Foi assim que Scala encontrou o implícito Numeric[Int]e, Numeric[Long]na sua pergunta, a propósito, como eles são encontrados no interior Numeric, não Integral.

Escopo implícito do tipo de um argumento

Se você tiver um método com um tipo de argumento A, o escopo implícito do tipo Atambém será considerado. Por "escopo implícito", quero dizer que todas essas regras serão aplicadas recursivamente - por exemplo, o objeto complementar de Aserá pesquisado por implícitos, conforme a regra acima.

Observe que isso não significa que o escopo implícito de Aserá pesquisado para conversões desse parâmetro, mas de toda a expressão. Por exemplo:

class A(val n: Int) {
  def +(other: A) = new A(n + other.n)
}
object A {
  implicit def fromInt(n: Int) = new A(n)
}

// This becomes possible:
1 + new A(1)
// because it is converted into this:
A.fromInt(1) + new A(1)

Está disponível desde o Scala 2.9.1.

Escopo implícito dos argumentos de tipo

Isso é necessário para que o padrão de classe de tipo realmente funcione. Considere Ordering, por exemplo: ele vem com alguns implícitos em seu objeto complementar, mas você não pode adicionar coisas a ele. Então, como você pode criar um Orderingpara sua própria classe que é encontrada automaticamente?

Vamos começar com a implementação:

class A(val n: Int)
object A {
    implicit val ord = new Ordering[A] {
        def compare(x: A, y: A) = implicitly[Ordering[Int]].compare(x.n, y.n)
    }
}

Portanto, considere o que acontece quando você liga

List(new A(5), new A(2)).sorted

Como vimos, o método sortedespera um Ordering[A](na verdade, ele espera um Ordering[B], onde B >: A). Não existe nada assim Orderinge não existe um tipo de "fonte" no qual procurar. Obviamente, está encontrando-o por dentro A, que é um argumento de tipoOrdering .

Também é assim que vários métodos de coleção que esperam o CanBuildFromtrabalho: os implícitos são encontrados nos objetos complementares nos parâmetros de tipo de CanBuildFrom.

Nota : Orderingé definido como trait Ordering[T], onde Té um parâmetro de tipo. Anteriormente, eu disse que o Scala olhou dentro dos parâmetros de tipo, o que não faz muito sentido. O implícito procurado acima é Ordering[A], onde Aé um tipo real, não um parâmetro de tipo: é um argumento de tipo para Ordering. Consulte a seção 7.2 da especificação Scala.

Está disponível desde o Scala 2.8.0.

Objetos externos para tipos aninhados

Na verdade, não vi exemplos disso. Ficaria grato se alguém pudesse compartilhar um. O princípio é simples:

class A(val n: Int) {
  class B(val m: Int) { require(m < n) }
}
object A {
  implicit def bToString(b: A#B) = "B: %d" format b.m
}
val a = new A(5)
val b = new a.B(3)
val s: String = b  // s == "B: 3"

Outras dimensões

Tenho certeza de que isso foi uma piada, mas essa resposta pode não estar atualizada. Portanto, não tome essa pergunta como árbitro final do que está acontecendo e, se você notou que ela ficou desatualizada, informe-me para que eu possa consertá-la.

EDITAR

Questões de interesse relacionadas:

Daniel C. Sobral
fonte
60
É hora de você começar a usar suas respostas em um livro, agora é só uma questão de juntar tudo.
pedrofurla
3
@pedrofurla Fui considerado um livro em português. Se alguém pode me localizar um contato com uma editora técnica ...
Daniel C. Sobral
2
Os objetos de pacote dos complementares das partes do tipo também são pesquisados. lampsvn.epfl.ch/trac/scala/ticket/4427
retronym
11
Nesse caso, faz parte do escopo implícito. O site de chamada não precisa estar dentro desse pacote. Isso foi surpreendente para mim.
retronym
2
Sim, então stackoverflow.com/questions/8623055 aborda isso especificamente, mas notei que você escreveu "A lista a seguir deve ser apresentada em ordem de precedência ... por favor, informe". Basicamente, as listas internas devem ser desordenadas, pois todas têm peso igual (pelo menos em 2.10).
Eugene Yokota
23

Eu queria descobrir a precedência da resolução implícita de parâmetros, não apenas onde ela procura, então escrevi uma postagem no blog revisitando implícitos sem imposto de importação (e a precedência implícita de parâmetros novamente após alguns comentários).

Aqui está a lista:

  • 1) implica visível para o escopo de chamada atual por meio de declaração local, importações, escopo externo, herança, objeto de pacote acessível sem prefixo.
  • 2) escopo implícito , que contém todos os tipos de objetos complementares e objetos de pacote que têm alguma relação com o tipo implícito que procuramos (ou seja, objeto de pacote do tipo, objeto complementar do próprio tipo, de seu construtor de tipo, se houver), seus parâmetros, se houver, e também de seus supertipos e supertraits).

Se em qualquer um dos estágios encontrarmos mais de uma regra implícita, a sobrecarga estática será usada para resolvê-la.

Eugene Yokota
fonte
3
Isso poderia ser melhorado se você escrevesse algum código apenas definindo pacotes, objetos, características e classes e usando suas letras quando se referir ao escopo. Não há necessidade de colocar nenhuma declaração de método - apenas nomes e quem estende quem e em que escopo.
Daniel C. Sobral