De uma maneira simples, quais são os limites de contexto e visualização e qual a diferença entre eles?
Alguns exemplos fáceis de seguir também seriam ótimos!
Eu pensei que isso já havia sido solicitado, mas, se assim for, a pergunta não é aparente na barra "relacionada". Então aqui está:
Um view bound foi um mecanismo introduzido no Scala para permitir o uso de algum tipo A
como se fosse algum tipo B
. A sintaxe típica é esta:
def f[A <% B](a: A) = a.bMethod
Em outras palavras, A
deve ter uma conversão implícita B
disponível, para que se possa chamar B
métodos em um objeto do tipo A
. O uso mais comum dos limites de exibição na biblioteca padrão (antes do Scala 2.8.0, de qualquer maneira) é com Ordered
, assim:
def f[A <% Ordered[A]](a: A, b: A) = if (a < b) a else b
Uma vez que se pode converter A
em um Ordered[A]
, e porque Ordered[A]
define o método <(other: A): Boolean
, que pode usar a expressão a < b
.
Esteja ciente de que os limites de exibição foram descontinuados . Você deve evitá-los.
Os limites de contexto foram introduzidos no Scala 2.8.0 e normalmente são usados com o chamado padrão de classe de tipo , um padrão de código que emula a funcionalidade fornecida pelas classes de tipo Haskell, embora de maneira mais detalhada.
Embora um limite de visualização possa ser usado com tipos simples (por exemplo A <% String
), um limite de contexto requer um tipo parametrizado , como Ordered[A]
acima, mas diferente String
.
Um limite de contexto descreve um valor implícito , em vez da conversão implícita do limite de exibição . É usado para declarar que, para algum tipo A
, existe um valor implícito do tipo B[A]
disponível. A sintaxe é assim:
def f[A : B](a: A) = g(a) // where g requires an implicit value of type B[A]
Isso é mais confuso do que a visualização vinculada, porque não está claro imediatamente como usá-lo. O exemplo comum de uso no Scala é este:
def f[A : ClassManifest](n: Int) = new Array[A](n)
Uma Array
inicialização em um tipo parametrizado exige ClassManifest
que esteja disponível, por razões misteriosas relacionadas ao apagamento de tipo e à natureza não apagável de matrizes.
Outro exemplo muito comum na biblioteca é um pouco mais complexo:
def f[A : Ordering](a: A, b: A) = implicitly[Ordering[A]].compare(a, b)
Aqui, implicitly
é usado para recuperar o valor implícito que queremos, um do tipo Ordering[A]
, cuja classe define o método compare(a: A, b: A): Int
.
Veremos outra maneira de fazer isso abaixo.
Não deve surpreender que os limites de visualização e de contexto sejam implementados com parâmetros implícitos, dada sua definição. Na verdade, a sintaxe que mostrei são açúcares sintáticos para o que realmente acontece. Veja abaixo como eles descongelam:
def f[A <% B](a: A) = a.bMethod
def f[A](a: A)(implicit ev: A => B) = a.bMethod
def g[A : B](a: A) = h(a)
def g[A](a: A)(implicit ev: B[A]) = h(a)
Portanto, naturalmente, é possível escrevê-los em sua sintaxe completa, o que é especialmente útil para os limites do contexto:
def f[A](a: A, b: A)(implicit ord: Ordering[A]) = ord.compare(a, b)
Os limites de exibição são usados principalmente para tirar proveito do padrão pimp my library , através do qual um "adiciona" métodos a uma classe existente, em situações em que você deseja retornar o tipo original de alguma forma. Se você não precisar retornar esse tipo de forma alguma, não precisará de uma visualização vinculada.
O exemplo clássico de uso vinculado à exibição é manipulação Ordered
. Observe que Int
não existe Ordered
, por exemplo, embora haja uma conversão implícita. O exemplo fornecido anteriormente precisa de uma visualização vinculada, pois retorna o tipo não convertido:
def f[A <% Ordered[A]](a: A, b: A): A = if (a < b) a else b
Este exemplo não funcionará sem limites de visualização. No entanto, se eu retornar outro tipo, não preciso mais de uma visualização vinculada:
def f[A](a: Ordered[A], b: A): Boolean = a < b
A conversão aqui (se necessário) acontece antes de eu passar o parâmetro para f
, portanto f
, não precisa saber sobre isso.
Além disso Ordered
, o uso mais comum da biblioteca é manipulação String
e Array
, que são classes Java, como se fossem coleções Scala. Por exemplo:
def f[CC <% Traversable[_]](a: CC, b: CC): CC = if (a.size < b.size) a else b
Se alguém tentasse fazer isso sem exibir limites, o tipo de retorno de a String
seria a WrappedString
(Scala 2.8) e da mesma forma para Array
.
O mesmo acontece mesmo se o tipo for usado apenas como um parâmetro de tipo do tipo de retorno:
def f[A <% Ordered[A]](xs: A*): Seq[A] = xs.toSeq.sorted
Os limites de contexto são usados principalmente no que ficou conhecido como padrão de classe de tipo , como uma referência às classes de tipo de Haskell. Basicamente, esse padrão implementa uma alternativa à herança, disponibilizando a funcionalidade por meio de um tipo de padrão implícito de adaptador.
O exemplo clássico é o Scala 2.8 Ordering
, que foi substituído Ordered
na biblioteca do Scala. O uso é:
def f[A : Ordering](a: A, b: A) = if (implicitly[Ordering[A]].lt(a, b)) a else b
Embora você normalmente veja isso escrito assim:
def f[A](a: A, b: A)(implicit ord: Ordering[A]) = {
import ord.mkOrderingOps
if (a < b) a else b
}
Que aproveitam algumas conversões implícitas internas Ordering
que permitem o estilo tradicional do operador. Outro exemplo no Scala 2.8 é o Numeric
:
def f[A : Numeric](a: A, b: A) = implicitly[Numeric[A]].plus(a, b)
Um exemplo mais complexo é o uso da nova coleção CanBuildFrom
, mas já existe uma resposta muito longa sobre isso, então evitarei aqui. E, como mencionado anteriormente, existe o ClassManifest
uso necessário para inicializar novas matrizes sem tipos concretos.
É muito mais provável que o contexto associado ao padrão de classe de tipo seja usado por suas próprias classes, pois elas permitem a separação de preocupações, enquanto os limites de exibição podem ser evitados em seu próprio código por um bom design (ele é usado principalmente para contornar o design de outra pessoa) )
Embora isso tenha sido possível há muito tempo, o uso de limites de contexto realmente decolou em 2010 e agora é encontrado até certo ponto na maioria das bibliotecas e estruturas mais importantes da Scala. O exemplo mais extremo de seu uso, porém, é a biblioteca Scalaz, que traz muito do poder de Haskell para Scala. Recomendo a leitura de padrões de classe para se familiarizar com todas as maneiras pelas quais ele pode ser usado.
EDITAR
Questões de interesse relacionadas: