Quais são as classs de tipo em Scala úteis?

Pelo que entendi desta postagem do blog “classs de tipo” no Scala é apenas um “padrão” implementado com características e adaptadores implícitos.

Como o blog diz que se eu tiver o traço A e um adaptador B -> A então eu posso invocar uma function, que requer o argumento do tipo A , com um argumento do tipo B sem chamar esse adaptador explicitamente.

Eu achei legal, mas não particularmente útil. Você poderia dar um caso de uso / exemplo, que mostra para que esse recurso é útil?

Um caso de uso, conforme solicitado …

Imagine que você tem uma lista de coisas, poderia ser números inteiros, números de ponto flutuante, matrizes, seqüências de caracteres, formas de onda, etc. Dada essa lista, você deseja adicionar o conteúdo.

Uma maneira de fazer isso seria ter algum atributo Addable que deve ser herdado por cada tipo que pode ser adicionado, ou uma conversão implícita para um Addable se estiver lidando com objects de uma biblioteca de terceiros que você não pode atualizar interfaces para .

Essa abordagem torna-se rapidamente avassaladora quando você também deseja começar a adicionar outras operações desse tipo que podem ser feitas em uma lista de objects. Também não funciona bem se você precisar de alternativas (por exemplo, adicionar duas formas de onda as concatena ou sobrepô-las?) A solução é um polymorphism ad-hoc, em que você pode escolher e escolher o comportamento a ser adaptado aos tipos existentes.

Para o problema original, você poderia implementar uma class de tipo Addable :

 trait Addable[T] { def zero: T def append(a: T, b: T): T } //yup, it's our friend the monoid, with a different name! 

Você pode então criar instâncias subclassificadas implícitas, correspondendo a cada tipo que você deseja tornar admissível:

 implicit object IntIsAddable extends Addable[Int] { def zero = 0 def append(a: Int, b: Int) = a + b } implicit object StringIsAddable extends Addable[String] { def zero = "" def append(a: String, b: String) = a + b } //etc... 

O método para sumr uma lista se torna trivial para escrever …

 def sum[T](xs: List[T])(implicit addable: Addable[T]) = xs.FoldLeft(addable.zero)(addable.append) //or the same thing, using context bounds: def sum[T : Addable](xs: List[T]) = { val addable = implicitly[Addable[T]] xs.FoldLeft(addable.zero)(addable.append) } 

A beleza dessa abordagem é que você pode fornecer uma definição alternativa de alguma typeclass, controlando o implícito desejado no escopo por meio de importações ou fornecendo explicitamente o argumento implícito. Assim, torna-se possível fornecer maneiras diferentes de adicionar formas de onda ou especificar a aritmética do módulo para adição de inteiros. Também é bastante indolor adicionar um tipo de alguma biblioteca de terceiros à sua typeclass.

Aliás, esta é exatamente a abordagem adotada pela API 2.8 collections. Embora o método sum seja definido em TraversableLike em vez de em List , e a class type seja Numeric (também contém mais algumas operações do que apenas zero e append )

Releia o primeiro comentário lá:

Uma distinção crucial entre classs de tipos e interfaces é que, para a class A ser um “membro” de uma interface, ela deve declarar no site de sua própria definição. Por outro lado, qualquer tipo pode ser incluído em uma class de texto a qualquer momento, desde que você possa fornecer as definições necessárias e, portanto, os membros de uma class de tipo em qualquer momento dependerão do escopo atual. Portanto, não nos importamos se o criador de A antecipou a class de tipos à qual queremos que pertença; se não, podemos simplesmente criar nossa própria definição, mostrando que de fato ela pertence, e depois usá-la de acordo. Portanto, isso não só fornece uma solução melhor do que os adaptadores, em certo sentido, elimina todo o problema que os adaptadores pretendiam resolver.

Eu acho que essa é a vantagem mais importante das classs de tipos.

Além disso, eles lidam adequadamente com os casos em que as operações não têm o argumento do tipo em que estamos distribuindo ou possuem mais de um. Por exemplo, considere este tipo de class:

 case class Default[T](val default: T) object Default { implicit def IntDefault: Default[Int] = Default(0) implicit def OptionDefault[T]: Default[Option[T]] = Default(None) ... } 

Penso em classs de tipo como a capacidade de adicionar metadados seguros de tipo a uma class.

Então, primeiro você define uma class para modelar o domínio do problema e depois pensa em metadados para adicioná-lo. Coisas como Equals, Hashable, Viewable, etc. Isso cria uma separação entre o domínio do problema e a mecânica para usar a class e abre a subclass porque a class é mais enxuta.

Exceto por isso, você pode adicionar classs de tipo em qualquer lugar no escopo, não apenas onde a class está definida e você pode alterar as implementações. Por exemplo, se eu calcular um código hash para uma class Point usando Point # hashCode, então estou limitado a essa implementação específica que pode não criar uma boa distribuição de valores para o conjunto específico de Pontos que tenho. Mas se eu usar o Hashable [Point], eu posso fornecer minha própria implementação.

[Atualizado com exemplo] Como exemplo, aqui está um caso de uso que tive na semana passada. Em nosso produto, há vários casos de mapas contendo contêineres como valores. Por exemplo, Map[Int, List[String]] ou Map[String, Set[Int]] . Adicionar a essas collections pode ser detalhado:

 map += key -> (value :: map.getOrElse(key, List())) 

Então eu queria ter uma function que envolvesse isso para que eu pudesse escrever

 map +++= key -> value 

A questão principal é que as collections não têm todos os mesmos methods para adicionar elementos. Alguns têm ‘+’ enquanto outros ‘: +’. Eu também queria manter a eficiência de adicionar elementos a uma lista, então eu não queria usar fold / map que criasse novas collections.

A solução é usar classs de tipo:

  trait Addable[C, CC] { def add(c: C, cc: CC) : CC def empty: CC } object Addable { implicit def listAddable[A] = new Addable[A, List[A]] { def empty = Nil def add(c: A, cc: List[A]) = c :: cc } implicit def addableAddable[A, Add](implicit cbf: CanBuildFrom[Add, A, Add]) = new Addable[A, Add] { def empty = cbf().result def add(c: A, cc: Add) = (cbf(cc) += c).result } } 

Aqui eu defini uma class de tipo Addable que pode adicionar um elemento C a uma coleção CC. Eu tenho duas implementações padrão: Para listas usando :: e para outras collections, usando a estrutura de construtor.

Então, usando este tipo de class é:

 class RichCollectionMap[A, C, B[_], M[X, Y] <: collection.Map[X, Y]](map: M[A, B[C]])(implicit adder: Addable[C, B[C]]) { def updateSeq[That](a: A, c: C)(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = { val pair = (a -> adder.add(c, map.getOrElse(a, adder.empty) )) (map + pair).asInstanceOf[That] } def +++[That](t: (A, C))(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = updateSeq(t._1, t._2)(cbf) } implicit def toRichCollectionMap[A, C, B[_], M[X, Y] <: col 

O bit especial está usando adder.add para adicionar os elementos e adder.empty para criar novas collections para novas chaves.

Para comparar, sem classs de tipo eu teria tido 3 opções: 1. escrever um método por tipo de coleção. Por exemplo, addElementToSubList e addElementToSet etc. Isso cria um monte de clichê na implementação e polui o namespace 2. para usar a reflection para determinar se a sub-coleção é um List / Set. Isso é complicado, pois o mapa está vazio para começar (claro que o scala também ajuda aqui com Manifests) 3. ter uma class de tipo pobre, exigindo que o usuário forneça o sumdor. Então, algo como addToMap(map, key, value, adder) , que é feio

Ainda outra maneira que eu acho útil neste blog é onde ele descreve typeclasss: Monads Are Not Metaphors

Procure no artigo por typeclass. Deve ser o primeiro jogo. Neste artigo, o autor fornece um exemplo de uma typeclass da Mônada.

Uma maneira de observar as classs de tipo é que elas permitem extensão retroativa ou polymorphism retroativo . Há alguns ótimos posts de Casual Miracles e Daniel Westheide que mostram exemplos de uso de Type Classes no Scala para conseguir isso.

Aqui está um post no meu blog que explora vários methods em escala de superposição retroativa , uma espécie de extensão retroativa, incluindo um exemplo típico.

O tópico do fórum ” O que torna as classs de tipos melhores que as características? ” Faz alguns pontos interessantes:

  • Typeclasss podem muito facilmente representar noções que são bastante difíceis de representar na presença de subtipagem, como igualdade e ordenação .
    Exercício: crie uma pequena hierarquia de classs / traços e tente implementar .equals em cada class / traço de tal forma que a operação sobre instâncias arbitrárias da hierarquia seja propriamente reflexiva, simétrica e transitiva.
  • Typeclasss permitem que você forneça evidências de que um tipo fora do seu “controle” está de acordo com algum comportamento.
    O tipo de outra pessoa pode ser um membro da sua typeclass.
  • Você não pode expressar “este método recebe / retorna um valor do mesmo tipo que o receptor do método” em termos de subtipagem, mas essa restrição (muito útil) é direta usando typeclasss. Este é o problema dos tipos limitados por f (onde um tipo limitado por F é parametrizado sobre seus próprios subtipos).
  • Todas as operações definidas em um traço requerem uma instância ; há sempre this argumento. Portanto, você não pode definir, por exemplo, um fromString(s:String): Foo no trait Foo de tal maneira que você possa chamá-lo sem uma instância de Foo .
    Em Scala isto se manifesta como pessoas tentando desesperadamente abstrair sobre objects de companhia.
    Mas é simples com uma typeclass, como ilustrado pelo elemento zero neste exemplo monóide .
  • Typeclasss pode ser definido indutivamente ; por exemplo, se você tiver um JsonCodec[Woozle] poderá obter um JsonCodec[List[Woozle]] gratuitamente.
    O exemplo acima ilustra isso para “coisas que você pode adicionar juntas”.

Não conheço nenhum outro caso de uso do que o polimorismo Ad-hoc, que é explicado aqui da melhor forma possível.

Ambos os implícitos e typeclasss são usados ​​para conversão de tipo . O principal caso de uso para ambos é fornecer um polymorphism ad-hoc (isto é) em classs que você não pode modificar, mas esperar um tipo de polymorphism de inheritance. No caso de implícitos, você poderia usar tanto uma class impl ou def implícita (que é sua class wrapper, mas oculta do cliente). Typeclasss são mais poderosos, pois podem adicionar funcionalidade a uma cadeia de inheritance já existente (por exemplo: Ordering [T] na function de sorting do scala). Para mais detalhes, você pode ver https://lakshmirajagopalan.github.io/diving-into-scala-typeclasss/

Em classs de tipo scala

  • Permite o polymorphism ad-hoc
  • Staticamente typescript (isto é, tipo seguro)
  • Emprestado de Haskell
  • Resolve o problema de expressão

O comportamento pode ser estendido – em tempo de compilation – após o fato – sem alterar / recompilar o código existente

Scala Implicits

A última lista de parâmetros de um método pode ser marcada como implícita

  • parameters implícitos são preenchidos pelo compilador

  • Na verdade, você precisa de evidências do compilador

  • … Como a existência de uma class de tipo no escopo

  • Você também pode especificar parâmetros explicitamente, se necessário

A extensão abaixo de Example na class String com implementação de class de tipo estende a class com um novo método, mesmo que a string seja final 🙂

 /** * Created by nihat.hosgur on 2/19/17. */ case class PrintTwiceString(val original: String) { def printTwice = original + original } object TypeClassString extends App { implicit def stringToString(s: String) = PrintTwiceString(s) val name: String = "Nihat" name.printTwice } 

Esta é uma diferença importante (necessária para functional programming):

insira a descrição da imagem aqui

Considere inc:Num a=> a -> a :

a recebido é o mesmo que é retornado, isso não pode ser feito com a subtipagem

Eu gosto de usar classs de tipo como uma forma idiomática de Injeção de Dependência Scala leve que ainda funciona com dependencies circulares, mas não adiciona muita complexidade de código. Recentemente reescrevi um projeto Scala de usar o Cake Pattern para digitar classs para DI e consegui uma redução de 59% no tamanho do código.

    Intereting Posts