Digite aulas em Scala3: um guia para iniciantes | Razão

Digite aulas em Scala3: um guia para iniciantes | Razão

Digite aulas em Scala3: um guia para iniciantes | Ledger PlatoBlockchain Data Intelligence. Pesquisa vertical. Ai.

Este documento é destinado ao desenvolvedor iniciante do Scala3 que já é versado na prosa do Scala, mas está intrigado com todos os `implicits` e características parametrizadas no código.

Este documento explica o porquê, como, onde e quando Classes de tipo (TC).

Depois de ler este documento o desenvolvedor iniciante em Scala3 ganhará sólido conhecimento para usar e mergulhar no código fonte do muito de bibliotecas Scala e comece a escrever código Scala idiomático.

Vamos começar com o porquê…

O problema da expressão

Em 1998, Philip Wadler afirmou que “a expressão problema é um novo nome para um problema antigo”. É o problema da extensibilidade do software. Segundo a escrita do senhor Wadler, a solução para o problema da expressão deve obedecer às seguintes regras:

  • Regra 1: Permitir a implementação de comportamentos existentes (pense no traço Scala) a ser aplicado a novas representações (pense em uma classe de caso)
  • Regra 2: Permitir a implementação de novos comportamentos para ser aplicado a representações existentes
  • Regra 3: Não deve comprometer a segurança de tipo
  • Regra 4: Não deve ser necessário recompilar código existente

Resolver este problema será o fio condutor deste artigo.

Regra 1: implementação do comportamento existente na nova representação

Qualquer linguagem orientada a objetos possui uma solução integrada para a regra 1 com polimorfismo de subtipo. Você pode implementar com segurança qualquer `trait` definido em uma dependência de um `class` em seu próprio código, sem recompilar a dependência. Vamos ver isso em ação:

Scala

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo


object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

val eth = Lib1.Ethereum()
val btc = Lib1.Bitcoin()
val dot = Lib2.Polkadot()

Neste exemplo fictício, a biblioteca `Lib1`(linha 5) define uma característica `Blockchain` (linha 6) com 2 implementações dele (linhas 9 e 12). `Lib1` permanecerá o mesmo em TODO este documento (aplicação da regra 4).

`Lib2`(linha 15) implementa o comportamento existente `Blockchain`em uma nova classe`Polkadot` (regra 1) de maneira segura (regra 3), sem recompilar `Lib1`(regra 4). 

Regra 2: implementação de novos comportamentos a serem aplicados às representações existentes

Vamos imaginar em `Lib2`queremos um novo comportamento`lastBlock` a ser implementado especificamente para cada `Blockchain`.

A primeira coisa que vem à mente é criar uma grande mudança com base no tipo de parâmetro.

Scala

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

 def lastBlock(blockchain: Blockchain): Block = blockchain match
 case _:Ethereum => todo
 case _:Bitcoin => todo
 case _:Polkadot => todo
 

object Lib3:
 import Lib1.*

 case class Polygon() extends Blockchain:
 override def getBlock(height: Height): Block = todo

import Lib1.*, Lib2.*, Lib3.*
println(lastBlock(Bitcoin()))
println(lastBlock(Ethereum()))
println(lastBlock(Polkadot()))
println(lastBlock(Polygon()))

Esta solução é uma reimplementação fraca do polimorfismo baseado em tipo que já está embutido na linguagem!

`Lib1` permanece intacto (lembre-se, a regra 4 é aplicada em todo este documento). 

A solução implementada em `Lib2 ok até que outro blockchain seja introduzido em `Lib3`. Ele infringe a regra de segurança de tipo (regra 3) porque este código falha em tempo de execução na linha 37. E modificando `Lib2` infringiria a regra 4.

Outra solução é usar um `extension`.

Scala

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

 def lastBlock(): Block = todo

 extension (eth: Ethereum) def lastBlock(): Block = todo

 extension (btc: Bitcoin) def lastBlock(): Block = todo

import Lib1.*, Lib2.*
println(Bitcoin().lastBlock())
println(Ethereum().lastBlock())
println(Polkadot().lastBlock())

def polymorphic(blockchain: Blockchain) =
 // blockchain.lastBlock()
 ???

`Lib1` permanece intacto (aplicação da regra 4 em todo o documento). 

`Lib2`define o comportamento para seu tipo (linha 21) e `extensões para tipos existentes (linhas 23 e 25).

Nas linhas 28 a 30, o novo comportamento pode ser usado em cada classe. 

Mas não há como chamar esse novo comportamento de polimorficamente (linha 32). Qualquer tentativa de fazer isso leva a erros de compilação (linha 33) ou a opções baseadas em tipo. 

Esta Regra nº 2 é complicada. Tentamos implementá-lo com nossa própria definição de polimorfismo e truque de “extensão”. E isso foi estranho.

Há uma peça faltando chamada polimorfismo ad hoc: a capacidade de despachar com segurança uma implementação de comportamento de acordo com um tipo, onde quer que o comportamento e o tipo sejam definidos. Introduzir o Classe de tipo padrão.

O padrão de classe de tipo

A receita do padrão Type Class (TC, para abreviar) tem 3 etapas. 

  1. Defina um novo comportamento
  2. Implementar o comportamento
  3. Utilize o comportamento

Na seção a seguir, implemento o padrão TC da maneira mais direta. É prolixo, desajeitado e impraticável. Mas espere, essas advertências serão corrigidas passo a passo no documento.

1. Defina um novo comportamento
Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

`Lib1` é, mais uma vez, deixado intocado.

O novo comportamento is o TC materializado pelo traço. As funções definidas no traço são uma forma de aplicar alguns aspectos desse comportamento.

O parâmetro `A` representa o tipo ao qual queremos aplicar o comportamento, que são subtipos de `Blockchain`no nosso caso.

Algumas observações:

  • Se necessário, o tipo parametrizado `A` pode ser ainda mais restringido pelo sistema de tipo Scala. Por exemplo, poderíamos impor `A`ser um`Blockchain`. 
  • Além disso, o TC poderia ter muito mais funções declaradas nele.
  • Finalmente, cada função pode ter muito mais parâmetros arbitrários.

Mas vamos manter as coisas simples para facilitar a leitura.

2. Implemente o comportamento
Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 val ethereumLastBlock = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 val bitcoinLastBlock = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

Para cada tipo o novo `LastBlock`comportamento é esperado, há uma instância específica desse comportamento. 

O `Ethereum` a linha de implementação 22 é calculada a partir do `eth`instância passada como parâmetro. 

A implementação de `LastBlock`para`Bitcoin` a linha 25 é implementada com um IO não gerenciado e não usa seu parâmetro.

Então, `Lib2`implementa novo comportamento `LastBlock`para`Lib1`aulas.

3. Use o comportamento
Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 val ethereumLastBlock = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 val bitcoinLastBlock = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A, behavior: LastBlock[A]) =
 behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2), ethereumLastBlock))
println(useLastBlock(Bitcoin(), bitcoinLastBlock))

Linha 30`useLastBlock` usa uma instância de `A` e o `LastBlock`comportamento definido para essa instância.

Linha 33`useLastBlock` é chamado com uma instância de `Ethereum` e uma implementação de `LastBlock` definido em `Lib2`. Observe que é possível passar qualquer implementação alternativa de `LastBlock[A]`(pense em Injeção de dependência).

`useLastBlock` é a cola entre a representação (o A real) e seu comportamento. Dados e comportamento são separados, e é isso que a programação funcional defende.

Discussão

Vamos recapitular as regras do problema de expressão:

  • Regra 1: Permitir a implementação de comportamentos existentes  para ser aplicado a novas aulas
  • Regra 2: Permitir a implementação de novos comportamentos para ser aplicado a classes existentes
  • Regra 3: Não deve comprometer a segurança de tipo
  • Regra 4: Não deve ser necessário recompilar código existente

A regra 1 pode ser resolvida imediatamente com polimorfismo de subtipo.

O padrão TC que acabamos de apresentar (veja a captura de tela anterior) resolve a regra 2. É de tipo seguro (regra 3) e nunca tocamos em `Lib1`(regra 4). 

No entanto, é impraticável usar por vários motivos:

  • Nas linhas 33-34, temos que passar explicitamente o comportamento ao longo de sua instância. Esta é uma sobrecarga extra. Deveríamos apenas escrever `useLastBlock(Bitcoin())`.
  • Na linha 31 a sintaxe é incomum. Preferimos escrever um texto conciso e mais orientado a objetos  `instance.lastBlock()`declaração.

Vamos destacar alguns recursos do Scala para uso prático do TC. 

Experiência aprimorada do desenvolvedor

Scala possui um conjunto único de recursos e açúcares sintáticos que tornam o TC uma experiência verdadeiramente agradável para os desenvolvedores.

Implícitos

O escopo implícito é um escopo especial resolvido em tempo de compilação onde apenas uma instância de um determinado tipo pode existir. 

Um programa coloca uma instância no escopo implícito com o `given`palavra-chave. Alternativamente, um programa pode recuperar uma instância do escopo implícito com a palavra-chave `using`.

O escopo implícito é resolvido em tempo de compilação, existe uma maneira de alterá-lo dinamicamente em tempo de execução. Se o programa for compilado, o escopo implícito será resolvido. Em tempo de execução, não é possível perder instâncias implícitas onde eles são usados. A única confusão possível pode vir do uso da instância implícita errada, mas esse problema fica para a criatura entre a cadeira e o teclado.

É diferente de um escopo global porque: 

  1. É resolvido contextualmente. Dois locais de um programa podem usar uma instância do mesmo tipo no escopo implícito, mas essas duas instâncias podem ser diferentes.
  2. Nos bastidores, o código está passando argumentos implícitos de função para função até que o uso implícito seja alcançado. Não está usando um espaço de memória global.

Voltando à aula de tipo! Vejamos exatamente o mesmo exemplo.

Scala

def todo = 42
type Height = Int
type Block = Int
def http(uri: String): Block = todo

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

`Lib1` é o mesmo código não modificado que definimos anteriormente. 

Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given ethereumLastBlock:LastBlock[Ethereum] = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given bitcoinLastBlock:LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A)(using behavior: LastBlock[A]) =
 behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2)))
println(useLastBlock(Bitcoin()))

Linha 19 um novo comportamento `LastBlock` é definido, exatamente como fizemos anteriormente.

Linha 22 e linha 25, `val`é substituído por`given`. Ambas as implementações de `LastBlock` são colocados no escopo implícito.

Linha 31`useLastBlock`declara o comportamento`LastBlock` como um parâmetro implícito. O compilador resolve a instância apropriada de `LastBlock`do escopo implícito contextualizado a partir dos locais do chamador (linhas 33 e 34). A linha 28 importa tudo de `Lib2`, incluindo o escopo implícito. Portanto, o compilador passa as instâncias definidas nas linhas 22 e 25 como o último parâmetro de `useLastBlock`. 

Como usuário de biblioteca, usar uma classe de tipo é mais fácil do que antes. Nas linhas 34 e 35, um desenvolvedor precisa apenas garantir que uma instância do comportamento seja injetada no escopo implícito (e isso pode ser um mero `import`). Se um implícito não for `given`onde está o código`using`isso, o compilador diz a ele.

Os implícitos do Scala facilitam a tarefa de passar instâncias de classe junto com instâncias de seus comportamentos.

Açúcares implícitos

As linhas 22 e 25 do código anterior podem ser melhoradas ainda mais! Vamos iterar nas implementações do TC.

Scala

given LastBlock[Ethereum] = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

Nas linhas 22 e 25, caso o nome da instância não seja utilizado, ele pode ser omitido.

Scala


 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

Nas linhas 22 e 25, a repetição do tipo pode ser substituída por `with`palavra-chave.

Scala

given LastBlock[Ethereum] = _.lastBlock

 given LastBlock[Bitcoin] = _ => http("http://bitcoin/last")

Como usamos uma característica degenerada com uma única função, o IDE pode sugerir simplificar o código com uma expressão SAM. Embora correto, não acho que seja um uso adequado do SAM, a menos que você esteja jogando golfe casualmente.

Scala oferece açúcares sintáticos para agilizar a sintaxe, removendo nomenclatura, declaração e redundância de tipo desnecessárias.

Extensão

Usado com sabedoria, o `extension` mecanismo pode simplificar a sintaxe para usar uma classe de tipo.

Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

 extension[A](instance: A)
 def lastBlock(using tc: LastBlock[A]) = tc.lastBlock(instance)

import Lib1.*, Lib2.*

println(Ethereum(lastBlock = 2).lastBlock)
println(Bitcoin().lastBlock)

Linhas 28-29 um método de extensão genérico `lastBlock`é definido para qualquer `A` com um `LastBlock` Parâmetro TC no escopo implícito.

Nas linhas 33-34, a extensão aproveita uma sintaxe orientada a objetos para usar TC.

Scala

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

 extension[A](instance: A)(using tc: LastBlock[A])
 def lastBlock = tc.lastBlock(instance)
 def penultimateBlock = tc.lastBlock(instance) - 1

import Lib1.*, Lib2.*

val eth = Ethereum(lastBlock = 2)
println(eth.lastBlock)
println(eth.penultimateBlock)

val btc = Bitcoin()
println(btc.lastBlock)
println(btc.penultimateBlock)

Na linha 28, o parâmetro TC também pode ser definido para toda a extensão para evitar repetições. Na linha 30 reutilizamos o TC na extensão para definir `penultimateBlock` (mesmo que possa ser implementado em `LastBlock`traço diretamente)

A mágica acontece quando o TC é usado. A expressão parece muito mais natural, dando a ilusão de que o comportamento `lastBlock`é confundido com a instância.

Tipo genérico com TC
Scala

import Lib1.*, Lib2.*

def useLastBlock1[A](instance: A)(using LastBlock[A]) = instance.lastBlock

def useLastBlock2[A: LastBlock](instance: A) = instance.lastBlock

val eth = Ethereum(lastBlock = 2)
assert(useLastBlock1(eth) == useLastBlock2(eth))

Na linha 34 a função usa um TC implícito. Observe que o TC não precisa ser nomeado se esse nome for desnecessário.

O padrão TC é tão amplamente utilizado que existe uma sintaxe de tipo genérica para expressar “um tipo com comportamento implícito”. Na linha 36 a sintaxe é uma alternativa mais concisa à anterior (linha 34). Evita declarar especificamente o parâmetro TC implícito sem nome.

Isso conclui a seção de experiência do desenvolvedor. Vimos como extensões, implícitos e algum açúcar sintático podem fornecer uma sintaxe menos confusa quando o TC é usado e definido.

Derivação automática

Muitas bibliotecas Scala usam TC, cabendo ao programador implementá-las em sua base de código.

Por exemplo, Circe (uma biblioteca de desserialização json) usa TC `Encoder[T]` e `Decoder[T]` para os programadores implementarem em sua base de código. Uma vez implementado, todo o escopo da biblioteca pode ser usado. 

Essas implementações de TC são mais do que frequentemente mapeadores orientados a dados. Eles não precisam de nenhuma lógica de negócios, são enfadonhos de escrever e difíceis de manter sincronizados com as classes de caso.

Em tal situação, essas bibliotecas oferecem o que é chamado automático derivação ou semi-automático derivação. Veja, por exemplo, Circe automático e semi-automático derivação. Com a derivação semiautomática, o programador pode declarar uma instância de uma classe de tipo com alguma sintaxe secundária, enquanto a derivação automática não necessita de nenhuma modificação de código, exceto uma importação.

Nos bastidores, em tempo de compilação, macros genéricas fazem introspecção tipos como pura estrutura de dados e gera um TC[T] para usuários da biblioteca. 

Derivar genericamente um TC é muito comum, então Scala introduziu uma caixa de ferramentas completa para esse propósito. Este método nem sempre é anunciado nas documentações da biblioteca, embora seja a maneira Scala 3 de usar derivação.

Scala

object GenericLib:

 trait Named[A]:
 def blockchainName(instance: A): String

 object Named:
 import scala.deriving.*

 inline final def derived[A](using inline m: Mirror.Of[A]): Named[A] =
 val nameOfType: String = inline m match
 case p: Mirror.ProductOf[A] => compiletime.constValue[p.MirroredLabel]
 case _ => compiletime.error("Not a product")
 new Named[A]:
 override def blockchainName(instance: A):String = nameOfType.toLowerCase

 extension[A] (instance: A)(using tc: Named[A])
 def blockchainName = tc.blockchainName(instance)

import Lib1.*, GenericLib.*

case class Polkadot() derives Named
given Named[Bitcoin] = Named.derived
given Named[Ethereum] = Named.derived

println(Ethereum(lastBlock = 2).blockchainName)
println(Bitcoin().blockchainName)
println(Polkadot().blockchainName)

Linha 18 um novo TC `Named`é introduzido. Este TC não está relacionado ao negócio de blockchain, estritamente falando. Seu objetivo é nomear o blockchain com base no nome da classe de caso.

Primeiro concentre-se nas linhas de definições 36-38. Existem 2 sintaxes para derivar um TC:

  1. Na linha 36, ​​a instância TC pode ser definida diretamente na classe de caso com o `derives`palavra-chave. Nos bastidores, o compilador gera um determinado `Named`instância em`Polkadot`objeto companheiro.
  2. Nas linhas 37 e 38, as instâncias de classes de tipo são fornecidas em classes pré-existentes com `TC.derived

Na linha 31 é definida uma extensão genérica (ver seções anteriores) e `blockchainName`é usado naturalmente.  

O `derives` palavra-chave espera um método com a forma `inline def derived[T](using Mirror.Of[T]): TC[T] = ???` que é definido na linha 24. Não vou explicar em detalhes o que o código faz. Em linhas gerais:

  • `inline def`define uma macro
  • `Mirror` faz parte da caixa de ferramentas para introspectar tipos. Existem diferentes tipos de espelhos, e na linha 26 o código se concentra em `Product`espelhos (uma classe de caso é um produto). Linha 27, se os programadores tentarem derivar algo que não seja um `Product`, o código não será compilado.
  • o `Mirror`contém outros tipos. Um deles, `MirrorLabel`, é uma string que contém o nome do tipo. Este valor é usado na implementação, linha 29, do `Named`TC.

Os autores de TC podem usar metaprogramação para fornecer funções que gerem genericamente instâncias de TC dado um tipo. Os programadores podem usar API de biblioteca dedicada ou ferramentas derivadas de Scala para criar instâncias para seu código.

Quer você precise de código genérico ou específico para implementar um TC, existe uma solução para cada situação. 

Resumo de todos os benefícios

  • Resolve o problema da expressão
    • Novos tipos podem implementar o comportamento existente por meio da herança tradicional de características
    • Novos comportamentos podem ser implementados em tipos existentes
  • Separação de preocupação
    • O código não é mutilado e pode ser facilmente excluído. Um TC separa dados e comportamento, que é um lema da programação funcional.
  • É seguro
    • É seguro porque não depende de introspecção. Evita grandes correspondências de padrões envolvendo tipos. se você estiver escrevendo esse código, poderá detectar um caso em que o padrão TC será adequado perfeitamente.
    • O mecanismo implícito é seguro para compilação! Se uma instância estiver faltando em tempo de compilação, o código não será compilado. Nenhuma surpresa em tempo de execução.
  • Traz polimorfismo ad-hoc
    • O polimorfismo ad hoc geralmente está ausente na programação tradicional orientada a objetos.
    • Com o polimorfismo ad-hoc, os desenvolvedores podem implementar o mesmo comportamento para vários tipos não relacionados sem usar a subtipagem tradicional (que acopla o código)
  • Injeção de dependência facilitada
    • Uma instância TC pode ser alterada em relação ao princípio de substituição de Liskov. 
    • Quando um componente depende de um TC, um TC simulado pode ser facilmente injetado para fins de teste. 

Contra-indicações

Cada martelo é projetado para uma série de problemas.

As classes de tipo são para problemas comportamentais e não devem ser usadas para herança de dados. Use composição para esse propósito.

A subtipagem usual é mais direta. Se você possui a base de código e não busca extensibilidade, as classes de tipo podem ser um exagero.

Por exemplo, no núcleo do Scala, existe um `Numeric`tipo classe:

Scala

trait Numeric[T] extends Ordering[T] {
 def plus(x: T, y: T): T
 def minus(x: T, y: T): T
 def times(x: T, y: T): T

Realmente faz sentido usar essa classe de tipo porque ela não apenas permite a reutilização de algoritmos algébricos em tipos incorporados em Scala (Int, BigInt, …), mas também em tipos definidos pelo usuário (um `ComplexNumber` por exemplo).

Por outro lado, a implementação de coleções Scala usa principalmente subtipagem em vez de classe de tipo. Este design faz sentido por vários motivos:

  • A API de coleta deve ser completa e estável. Ele expõe o comportamento comum por meio de características herdadas pelas implementações. Ser altamente extensível não é um objetivo específico aqui.
  • Deve ser simples de usar. O TC adiciona uma sobrecarga mental ao programador do usuário final.
  • O TC também pode incorrer em pequenas sobrecargas no desempenho. Isto pode ser crítico para uma API de coleção.
  • Porém, a API de coleção ainda é extensível por meio de novos TC definidos por bibliotecas de terceiros.

Conclusão

Vimos que o TC é um padrão simples que resolve um grande problema. Graças à rica sintaxe do Scala, o padrão TC pode ser implementado e usado de várias maneiras. O padrão TC está alinhado com o paradigma de programação funcional e é uma ferramenta fabulosa para uma arquitetura limpa. Não existe solução mágica e o padrão TC deve ser aplicado quando for adequado.

Espero que você tenha adquirido conhecimento lendo este documento. 

O código está disponível em https://github.com/jprudent/type-class-article. Entre em contato comigo se tiver qualquer tipo de dúvida ou comentário. Você pode usar problemas ou comentários de código no repositório, se desejar.


Jerônimo PRUDENTE

Engenheiro de Software

Carimbo de hora:

Mais de Ledger