introdução ao typeclasses em Scala

  • Parte 1: Este post
  • Parte 2: Segurança do Tipo de serialização em Scala
  • Parte 3: Typeful o tratamento de erros com maior kinded tipos de

o Que é uma classe do tipo

Uma das maiores barreiras para a compreensão e utilização de classes de tipo em seu próprio código é o próprio nome. Ao contrário do que o nome evoca, uma classe de tipo não é uma classe de uma linguagem orientada a objetos. De acordo com a wikipedia:

Em Ciência da computação, uma classe de tipo é uma construção de Sistema de tipo que suporta polimorfismo ad hoc. Isso é conseguido adicionando restrições a variáveis de tipo em tipos parametricamente polimórficos.

temos algumas descompactações para fazer:

  • o que é polimorfismo “ad hoc”? Seja o que for, é isso que as classes de tipo nos ajudarão a alcançar.
  • o que é um tipo ‘parametricamente polimórfico’? Seja o que for, é assim que vamos criar nossas próprias classes de tipo.

classes de tipo apareceram pela primeira vez em Haskell, e são uma maneira fundamental de construir abstrações em Haskell. Eles são tão vitais que são introduzidos na primeira página do livro Learn You a Haskell:

uma classe de tipo é um tipo de interface que define algum comportamento. Se um tipo é uma parte de uma classe de tipo, isso significa que ele suporta e implementa o comportamento que a classe de tipo descreve.

Ok, então parece que estamos tentando definir algum comportamento para nossos tipos e implementar esse comportamento de maneira diferente dependendo do tipo. Essa última parte, “implementar o comportamento de forma diferente dependendo do tipo” é o bit ‘polimorfismo’ de antes. Portanto, o termo está começando a fazer sentido: é uma classe para tipos. Diz — se que uma classe que implementa o comportamento designado pela classe de tipo é um membro dessa classe de tipos – ‘classe de tipo’.

neste ponto, seria apropriado dizer que isso soa como herança. Ambos nos permitem construir abstrações e fornecer polimorfismo.

então, por que explorar classes de tipo? Em muitos casos, eles são um pouco mais poderosos e extensíveis do que uma hierarquia de herança. Eles podem ser muito mais simples, e são um ajuste muito melhor quando sua base de código já se inclina FP sobre oo. Embora, mesmo em um projeto fortemente OO, as classes de tipo ainda possam salvar o dia.

definindo um typeclass

isso é Introdução suficiente – para o resto do artigo, exploraremos uma funcionalidade muito familiar, que é realmente implementada em Java por herança e implementá-la usando uma classe de tipo: stringification.

estamos todos familiarizados com o método que existe em todos os objetos em Java (e, portanto, Scala): .toString. Seu objetivo é produzir uma representação de string do objeto em questão. Vamos ver como usaríamos classes de tipo para fazer isso. A versão de Haskell de toString é chamada Show, então vamos ficar mais ou menos com esse nome:

trait Showable {
def show(a: A): String
}

é isso: há nossa classe de tipo Showable. Por que é preciso um parâmetro de tipo A? Se nos lembrarmos de nossa primeira definição de classes de tipo, lemos que ” alcançado adicionando restrições a variáveis de tipo em tipos parametricamente polimórficos.”Showable é o nosso tipo parametricamente polimórfico, e A é a nossa variável de tipo, o que significa simplesmente que leva um tipo (A) como um parâmetro de tipo. (Observe que não colocamos nenhuma restrição em A, de acordo com a definição, mas tudo bem. Isso é algo que fazemos ao compor classes de tipo juntos, ou fazendo uso deles, que virá mais tarde.)

implementando um typeclass

agora devemos fornecer implementações do traço Showable para qualquer tipo que desejamos ser capazes de mostrar. Essas implementações são chamadas de “instâncias” de nossa classe de tipo, uma vez que serão literalmente instâncias do traço Showable. Na maioria das vezes, serão classes anônimas que substituem .show e, como há algum clichê envolvido na definição delas (lembre-se de ‘objetos de função’ na programação Java pré-lambda?), definiremos um auxiliar chamado make. Vamos começar com uma implementação para um tipo de nome de usuário:

object Showable {
def make(showFn: A => String): Showable = new Showable {
override def show(a: A): String = showFn(a)
}
}final case class UserName(first: String, last: String)implicit val defaultUserNameShowable = Showable.make(userName => s"${userName.first} ${userName.last}")val janeName = UserName("Jane", "Doe")

então, como invocaríamos isso? Poderíamos chamar diretamente o método da instância showable, assim:

defaultUserNameShowable.show(janeName) // "Jane Doe"

sintaxe aprimorada

conseguimos invocar nossa implementação Showable de UserName, mas não foi muito bom. Tivemos que ter uma referência à nossa instância de classe de tipo ao redor (defaultUserNameShowable) e saber seu nome. Isso é algo que queremos abstrair. Se a história terminasse aqui, as classes de tipo no Scala não seriam tão boas de usar. Felizmente, podemos melhorar a ergonomia drasticamente usando implicits. Aumentaremos o objeto companheiro de Showable com um auxiliar de Invocação:

implicit class ShowSyntax(a: A) extends AnyVal {
def show(implicit showable: Showable): String = showable.show(a)
}
...
janeName.show

ao colocar este “método de extensão” em algum lugar no escopo, podemos simplesmente fingir que todos os objetos têm um método .show.

o que isso alcançou

então, por que não apenas substituir toString em UserName em vez disso? Bem, digamos que UserName vive em uma biblioteca da qual dependemos, e já está compilado pelo tempo que estamos consumindo. Não podemos subclassificá-lo, pois é uma classe final. No entanto, usando classes de tipo, estamos livres para essencialmente “anexar” novo comportamento a ele de uma forma ad hoc. Esta é a parte “ad hoc” da definição acima: podemos adicionar esse polimorfismo separadamente do local onde o próprio tipo é definido.

além disso, ao contrário da herança, quando definimos classes de tipo no Scala, elas não precisam ser ‘coerentes’, o que significa que podemos definir várias implementações para Showable e escolher entre elas. Para algumas classes de tipo, isso é uma coisa boa, para outras não tanto.

em Haskell, as classes de tipo são coerentes. No Scala 3, de acordo com Martin Odersky, teremos a capacidade de escolher, para cada classe de tipo, se deve ser coerente ou não.

mas por enquanto, vamos fornecer um significado alternativo de show para UserName:

implicit val secretUserNameShowable = Showable.make(_ => "<secret person>")

tudo o que precisamos fazer agora é ter certeza de que apenas uma de nossas instâncias Showable está em escopo por vez. E se não, não é grande coisa, obteremos um erro de ‘valores implícitos ambíguos’ em tempo de compilação:

janeName.show
> ambiguous implicit values:
both value secretUserNameShowable in object typeclasses of type => Showable
and value defaultUserNameShowable in object typeclasses of type => Showable
match expected type Showable

uma nota importante é que não acoplamos UserName e Showable. Tudo o que fizemos foi definir um significado de Showable para UserName, mantendo-os completamente independentes um do outro. Essa dissociação tem enormes implicações de manutenção e ajuda a evitar relacionamentos de dependência emaranhados em todo o seu código.

segurança em tempo de Compilação

o que acontece se tentarmos chamar .show em um objeto para o qual não fornecemos um significado de show? Em outras palavras, quando não há instância de Showable em escopo para nosso alvo? Simplesmente obtemos um erro do compilador:

java.util.UUID.randomUUID().show
> could not find implicit value for parameter showable: Showable

podemos realmente personalizar essa mensagem de erro quando definimos nossa classe de tipo:

@scala.annotation.implicitNotFound("No Showable in scope for A = ${A}. Try importing or defining one.")
trait Showable {
...
}
...
janeName.show
> No Showable in scope for A = java.util.UUID. Try importing or defining one.

key Takeaways

  • classes de tipo nos permitem definir um conjunto de comportamentos totalmente separadamente dos objetos e tipos que implementarão esses comportamentos.
  • classes de tipo são expressas em Scala puro com características que tomam parâmetros de tipo e implicita para tornar a sintaxe limpa.
  • classes de tipo nos permitem estender ou implementar comportamento para tipos e objetos cuja fonte não podemos modificar. (Eles fornecem o polimorfismo ad hoc necessário para resolver o problema de expressão).

o que vem a seguir

concedido, Showable é uma classe de tipo trivial, embora não sem seus usos. Podemos facilmente imaginar muito mais úteis, no entanto:

trait SafeSerializable {
def serialize(a: A): Array
def deserialize(bytes: Array): A
}
trait Convertible {
def to(a: A): B
def from(b: B): A
}
trait Functor] {
def map(a: F, f: A => B): F
}

SafeSerializable pode ser uma ótima alternativa para algo como a serialização Akka, onde esperamos até o tempo de execução para ver se um serializador está registrado ou não para o nosso tipo. É muito mais seguro passar implicitamente sua implementação de serialização em tempo de compilação!

Convertible pode ser uma ótima maneira reutilizável de converter dados em vários tipos. Há uma biblioteca do Twitter com o mesmo objetivo em mente chamado Bijection.

Functor é um bloco de construção para algumas abstrações extremamente poderosas em uma área de programação funcional chamada ‘programação categórica’. Bibliotecas como ScalaZ e Cats são construídas em classes de tipo como Functor. Uma noção importante que não discutimos desta vez é que as classes de tipo podem ser facilmente compostas para construir um comportamento cada vez mais complexo a partir de blocos de construção simples como Functor.Além disso, muitas bibliotecas essenciais no ecossistema Scala (jogar JSON, Slick, et. al) faça uso de classes de tipo e exponha-as como o principal driver de sua API em muitos casos (E.G. Jogue a classe de tipo Format do JSON). Entender as classes de tipo você mesmo é uma ótima maneira de se tornar mais eficaz em fazer uso do que está lá fora no ecossistema Scala, mesmo que você nunca escreva o seu próprio.

no futuro, estarei olhando para algumas classes de tipo imediatamente práticas como SafeSerializable . Para uma introdução às classes de tipo categórico mais abstratas, como Functor] e Monad], não procure mais do que a documentação da biblioteca cats. Para um guia mais detalhado para programação em Scala usando classes de tipo, o underscore.io tem um livro fantástico sobre o assunto: Scala com gatos

Parte 2: serialização Tipo-segura em Scala

GIST com código

Deixe uma resposta

O seu endereço de email não será publicado.