O que é um “limite de contexto” em Scala?

115

Um dos novos recursos do Scala 2.8 são os limites de contexto. O que é um limite de contexto e onde ele é útil?

Claro que pesquisei primeiro (e encontrei, por exemplo, isto ), mas não consegui encontrar nenhuma informação realmente clara e detalhada.

Jesper
fonte
8
também verifique isso para um tour por todos os tipos de limites: gist.github.com/257758/47f06f2f3ca47702b3a86c76a5479d096cb8c7ec
Arjan Blokzijl
2
Esta excelente resposta compara / contrasta os limites de contexto e de visualização: stackoverflow.com/questions/4465948/…
Aaron Novstrup
Esta é uma resposta muito boa stackoverflow.com/a/25250693/1586965
samthebest

Respostas:

107

Você achou este artigo ? Ele cobre o novo recurso de limite de contexto, dentro do contexto de melhorias de array.

Geralmente, um parâmetro de tipo com um limite de contexto é do formulário [T: Bound]; ele é expandido para um parâmetro de tipo simples Tjunto com um parâmetro implícito de tipo Bound[T].

Considere o método tabulateque forma uma matriz a partir dos resultados da aplicação de uma dada função f em um intervalo de números de 0 até um determinado comprimento. Até Scala 2.7, tabular poderia ser escrito da seguinte forma:

def tabulate[T](len: Int, f: Int => T) = {
    val xs = new Array[T](len)
    for (i <- 0 until len) xs(i) = f(i)
    xs
}

No Scala 2.8 isso não é mais possível, porque as informações de tempo de execução são necessárias para criar a representação correta do Array[T]. É necessário fornecer essas informações passando um ClassManifest[T]para o método como um parâmetro implícito:

def tabulate[T](len: Int, f: Int => T)(implicit m: ClassManifest[T]) = {
    val xs = new Array[T](len)
    for (i <- 0 until len) xs(i) = f(i)
    xs
}

Como uma forma abreviada, um limite de contexto pode ser usado no parâmetro de tipo T, fornecendo:

def tabulate[T: ClassManifest](len: Int, f: Int => T) = {
    val xs = new Array[T](len)
    for (i <- 0 until len) xs(i) = f(i)
    xs
}
Robert Harvey
fonte
145

A resposta de Robert cobre os detalhes técnicos de Context Bounds. Vou lhe dar minha interpretação de seu significado.

Em Scala, um View Bound ( A <% B) captura o conceito de 'pode ser visto como' (enquanto um limite superior <:captura o conceito de 'é um'). Um limite de contexto ( A : C) diz 'tem um' sobre um tipo. Você pode ler os exemplos sobre manifestos como " Ttem um Manifest". O exemplo que você vinculou a cerca de Orderedvs Orderingilustra a diferença. Um método

def example[T <% Ordered[T]](param: T)

diz que o parâmetro pode ser visto como um Ordered. Compare com

def example[T : Ordering](param: T)

que diz que o parâmetro tem um associado Ordering.

Em termos de uso, demorou um pouco para que as convenções fossem estabelecidas, mas os limites de contexto são preferíveis aos limites de visualização (os limites de visualização agora estão obsoletos ). Uma sugestão é que um limite de contexto seja preferido quando você precisa transferir uma definição implícita de um escopo para outro sem precisar se referir a ela diretamente (este é certamente o caso do ClassManifestusado para criar um array).

Outra maneira de pensar sobre limites de visão e limites de contexto é que o primeiro transfere conversões implícitas do escopo do chamador. O segundo transfere objetos implícitos do escopo do chamador.

Ben Lings
fonte
2
"tem um" em vez de "é um" ou "visto como" foi o insight chave para mim - não visto em nenhuma outra explicação. Ter uma versão simples em inglês dos operadores / funções que de outra forma seriam um pouco crípticos torna muito mais fácil de absorver - obrigado!
DNA de
1
@Ben Lings O que você quer dizer com .... 'tem um' sobre um tipo ...? O que é um tipo ?
jhegedus de
1
@jhegedus Aqui está minha análise: "sobre um tipo" significa que A se refere a um tipo. A frase "tem um" é freqüentemente usada em design orientado a objetos para descrever relacionamentos de objetos (por exemplo, o cliente "tem um" endereço). Mas aqui a relação "tem um" é entre tipos, não objetos. É uma analogia vaga porque o relacionamento "tem um" não é inerente ou universal do jeito que é no design OO; um cliente sempre tem um endereço, mas para o limite de contexto, um A nem sempre tem um C. Em vez disso, o limite de contexto especifica que uma instância de C [A] deve ser fornecida implicitamente.
jbyler
Estou aprendendo Scala há um mês e esta é a melhor explicação que vi neste mês! Obrigado @Ben!
Lifu Huang
@Ben Lings: Obrigado, depois de passar tanto tempo entendendo o que é vinculado ao contexto, sua resposta é muito útil. [ has aFaz mais sentido para mim]
Shankar
39

(Esta é uma nota entre parênteses. Leia e entenda as outras respostas primeiro.)

Os limites de contexto realmente generalizam os limites de visualização.

Portanto, dado este código expresso com um View Bound:

scala> implicit def int2str(i: Int): String = i.toString
int2str: (i: Int)String

scala> def f1[T <% String](t: T) = 0
f1: [T](t: T)(implicit evidence$1: (T) => String)Int

Isso também pode ser expresso com um limite de contexto, com a ajuda de um alias de tipo que representa funções de tipo Fpara tipo T.

scala> trait To[T] { type From[F] = F => T }           
defined trait To

scala> def f2[T : To[String]#From](t: T) = 0       
f2: [T](t: T)(implicit evidence$1: (T) => java.lang.String)Int

scala> f2(1)
res1: Int = 0

Um limite de contexto deve ser usado com um construtor de tipo do tipo * => *. No entanto, o construtor Function1de tipo é do tipo (*, *) => *. O uso do alias de tipo aplica parcialmente o segundo parâmetro de tipo com o tipoString , produzindo um construtor de tipo do tipo correto para uso como um limite de contexto.

Existe uma proposta para permitir que você expresse diretamente tipos parcialmente aplicados em Scala, sem o uso do apelido de tipo dentro de uma característica. Você poderia então escrever:

def f3[T : [X](X => String)](t: T) = 0 
retrônimo
fonte
Você poderia explicar o significado de #De na definição de f2? Não tenho certeza de onde o tipo F está sendo construído (eu disse isso corretamente?)
Collin
1
É chamado de projeção de tipo, referenciando um membro Fromde tipo do tipo To[String]. Não fornecemos um argumento de tipo para From, portanto, estamos nos referindo ao construtor de tipo, não a um tipo. Este construtor de tipo é do tipo certo para ser usado como um limite de contexto - * -> *. Isso limita o parâmetro de tipo T, exigindo um parâmetro implícito de tipo To[String]#From[T]. Expanda os aliases de tipo e pronto, você fica com Function1[String, T].
retrônimo de
deve ser Function1 [T, String]?
ssanj
18

Esta é outra nota entre parênteses.

Como Ben apontou , um limite de contexto representa uma restrição "tem-a" entre um parâmetro de tipo e uma classe de tipo. Dito de outra forma, ele representa uma restrição de que existe um valor implícito de uma classe de tipo particular.

Ao utilizar um limite de contexto, muitas vezes é necessário revelar esse valor implícito. Por exemplo, dada a restrição T : Ordering, muitas vezes será necessária a instância de Ordering[T]que satisfaça a restrição. Conforme demonstrado aqui , é possível acessar o valor implícito usando o implicitlymétodo ou um método um pouco mais útil context:

def **[T : Numeric](xs: Iterable[T], ys: Iterable[T]) = 
   xs zip ys map { t => implicitly[Numeric[T]].times(t._1, t._2) }

ou

def **[T : Numeric](xs: Iterable[T], ys: Iterable[T]) =
   xs zip ys map { t => context[T]().times(t._1, t._2) }
Aaron Novstrup
fonte