it-swarm-pt.tech

Conversão implícita vs. classe de tipo

No Scala, podemos usar pelo menos dois métodos para modernizar tipos existentes ou novos. Suponha que queremos expressar que algo pode ser quantificado usando um Int. Podemos definir a seguinte característica.

Conversão implícita

trait Quantifiable{ def quantify: Int }

E então podemos usar conversões implícitas para quantificar, por exemplo Strings e listas.

implicit def string2quant(s: String) = new Quantifiable{ 
  def quantify = s.size 
}
implicit def list2quantifiable[A](l: List[A]) = new Quantifiable{ 
  val quantify = l.size 
}

Depois de importá-los, podemos chamar o método quantify em strings e listas. Observe que a lista quantificável armazena seu comprimento, evitando assim o deslocamento da lista em chamadas subseqüentes para quantify.

Classes de tipos

Uma alternativa é definir uma "testemunha" Quantified[A] afirma que algum tipo A pode ser quantificado.

trait Quantified[A] { def quantify(a: A): Int }

Em seguida, fornecemos instâncias dessa classe de tipo para String e List em algum lugar.

implicit val stringQuantifiable = new Quantified[String] {
  def quantify(s: String) = s.size 
}

E se escrevermos um método que precisa quantificar seus argumentos, escreveremos:

def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) = 
  as.map(ev.quantify).sum

Ou usando a sintaxe vinculada ao contexto:

def sumQuantities[A: Quantified](as: List[A]) = 
  as.map(implicitly[Quantified[A]].quantify).sum

Mas quando usar qual método?

Agora vem a pergunta. Como posso decidir entre esses dois conceitos?

O que eu notei até agora.

classes de tipo

  • classes de tipo permitem a sintaxe ligada ao contexto Nice
  • com classes de tipo, não crio um novo objeto wrapper em cada uso
  • a sintaxe vinculada ao contexto não funcionará mais se a classe type tiver vários parâmetros de tipo; imagine que eu quero quantificar as coisas não apenas com números inteiros, mas com valores de algum tipo geral T. Eu gostaria de criar uma classe de tipo Quantified[A,T]

conversão implícita

  • desde que eu criei um novo objeto, posso armazenar em cache valores lá ou calcular uma melhor representação; mas devo evitar isso, pois isso pode acontecer várias vezes e uma conversão explícita provavelmente seria invocada apenas uma vez?

O que eu espero de uma resposta

Apresente um (ou mais) casos de uso em que a diferença entre os dois conceitos é importante e explique por que eu preferiria um ao outro. Também explicar a essência dos dois conceitos e sua relação um com o outro seria bom, mesmo sem exemplo.

92
ziggystar

Embora eu não queira duplicar meu material de Scala In Depth , acho que vale a pena notar que classes de tipo/traços de tipo são infinitamente mais flexíveis.

def foo[T: TypeClass](t: T) = ...

tem a capacidade de procurar em seu ambiente local uma classe de tipo padrão. No entanto, posso substituir o comportamento padrão a qualquer momento por uma de duas maneiras:

  1. Criando/Importando uma Instância de Classe de Tipo Implícito no Escopo para Curto-Circuito da Pesquisa Implícita
  2. Passando diretamente uma classe de tipo

Aqui está um exemplo:

def myMethod(): Unit = {
   // overrides default implicit for Int
   implicit object MyIntFoo extends Foo[Int] { ... }
   foo(5)
   foo(6) // These all use my overridden type class
   foo(7)(new Foo[Int] { ... }) // This one needs a different configuration
}

Isso torna as classes de tipos infinitamente mais flexíveis. Outra coisa é que classes/características de tipo suportam melhor a pesquisa implícita .

No seu primeiro exemplo, se você usar uma exibição implícita, o compilador fará uma pesquisa implícita para:

Function1[Int, ?]

Que examinará o objeto complementar de Function1 E o objeto complementar de Int.

Observe que Quantifiable não está em lugar algum na pesquisa implícita. Isso significa que você deve colocar a visão implícita em um objeto de pacote ou importá-la para o escopo. É mais trabalho lembrar o que está acontecendo.

Por outro lado, uma classe de tipo é explícita . Você vê o que está procurando na assinatura do método. Você também tem uma pesquisa implícita

Quantifiable[Int]

que procurará no objeto complementar de Quantifiable e objeto complementar de Int. O que significa que você pode fornecer padrões e novos tipos (como uma classe MyString) podem fornecer um padrão em seu objeto complementar e será implicitamente pesquisado.

Em geral, eu uso classes de tipos. Eles são infinitamente mais flexíveis para o exemplo inicial. O único lugar em que uso conversões implícitas é ao usar uma camada de API entre uma Scala wrapper e uma Java biblioteca, e mesmo isso pode ser 'perigosa' se você não tome cuidado.

41
jsuereth

Um critério que pode entrar em jogo é como você deseja que o novo recurso "pareça"; usando conversões implícitas, você pode fazer parecer que é apenas outro método:

"my string".newFeature

... enquanto estiver usando classes de tipo, sempre parecerá que você está chamando uma função externa:

newFeature("my string")

Uma coisa que você pode obter com classes de tipo e não com conversões implícitas é adicionar propriedades a type, em vez de a uma instância de um tipo. Você pode acessar essas propriedades mesmo quando você não tem uma instância do tipo disponível. Um exemplo canônico seria:

trait Default[T] { def value : T }

implicit object DefaultInt extends Default[Int] {
  def value = 42
}

implicit def listsHaveDefault[T : Default] = new Default[List[T]] {
  def value = implicitly[Default[T]].value :: Nil
}

def default[T : Default] = implicitly[Default[T]].value

scala> default[List[List[Int]]]
resN: List[List[Int]] = List(List(42))

Este exemplo também mostra como os conceitos estão intimamente relacionados: classes de tipos não seriam tão úteis se não houvesse mecanismo para produzir infinitamente muitas de suas instâncias; sem o método implicit (não é uma conversão, é verdade), eu só poderia ter muitos tipos finitos com a propriedade Default.

20
Philippe

Você pode pensar na diferença entre as duas técnicas por analogia ao aplicativo de função, apenas com um invólucro nomeado. Por exemplo:

trait Foo1[A] { def foo(a: A): Int }  // analogous to A => Int
trait Foo0    { def foo: Int }        // analogous to Int

Uma instância do primeiro encapsula uma função do tipo A => Int, enquanto uma instância deste último já foi aplicada a um A. Você pode continuar o padrão ...

trait Foo2[A, B] { def foo(a: A, b: B): Int } // sort of like A => B => Int

assim você poderia pensar em Foo1[B] mais ou menos como a aplicação parcial de Foo2[A, B] para alguma instância A. Um ótimo exemplo disso foi escrito por Miles Sabin como "Dependências funcionais no Scala" .

Então, na verdade, meu argumento é que, em princípio:

  • "pimping" uma classe (por meio de conversão implícita) é o caso "ordem zero '" ...
  • declarar uma classe de tipo é o caso "de primeira ordem" ...
  • tipeclasses multiparâmetros com fundeps (ou algo como fundeps) é o caso geral.
13
mergeconflict