DEV Community

Terminal Coffee
Terminal Coffee

Posted on

Como classificar a sua linguagem - Introdução a sistema de tipos

Introdução

Quando o assunto é linguagens de programação dois termos muito utilizados para classifica-las são: fortemente tipada, e fracamente tipada, o problema com essa abordagem é que ambos são termos meio subjetivos quanto ao seu significado, o que dificulta um debate sério ao tentar classificar algo como forte ou fracamente tipado. Entretanto, a teoria sobre sistemas de tipos nos fornece conceitos mais úteis nessa tarefa de classificar uma linguagem de programação pelo seu sistema de tipos.

TL;DR;

Podemos classificar um sistema de tipos pelas suas características:

  • Se é dinâmico ou estático;
  • Se é estrutural ou nominal;
  • Se a tipagem é implícita ou explícita;

Dinâmico vs estático

O primeiro critério que podemos usar para classificar propriamente um sistema de tipos, seria se o processo de checar e reforçar as restrições dos tipos ocorre em compile-time ou em run-time.

Em uma linguagem onde a checagem durante a execução do programa, ela ocorre em runtime, ou seja, ela seria considerada uma linguagem dinâmica.

Da mesma forma, se a checagem ocorre antes do código rodar, apenas analisando o conteúdo do código através de uma ferramenta, seja ela um analisador estático, o compilador, um linter, e etc, ela é considerada como ocorrendo em compile-time, dessa forma uma linguagem onde isso acontece seria uma com sistema de tipos estático.

Linguagens dinâmicas geralmente são mais simples de executar, e por isso são associadas com curvas menores de aprendizado, além de não necessariamente exigirem que os tipos sejam declarados explícitamente (tópico que iremos abordar em breve), tornando o aprendizado inicial bem mais simples, facilitando abordagens mais interativas de desenvolvimento (escrever o programa, rodar para testar, e repetir o ciclo).

Enquanto isso, as estáticas geralmente acabam por serem um pouco mais complicadas de executar devido a exigência de uma validação quanto a se o programa está "correto" antes que ele possa ser executado, resultando nas famosas experiências onde "o código não compila", em contrapartida, oferecem uma segurança maior quanto ao programa que vai ser executado, uma vez que esses tipos de erros são pegos durante o desenvolvimento, e não de surpresa depois que o código já está rodando em produção, e algum usuário faz algo que não devia, e acaba recebendo um erro inesperado na cara.

Apesar de linguagens dinâmicas serem associadas com uma experiência de desenvolvimento (DX) melhor, as estáticas possuem uma qualidade nesse quesito bem significativa, que é a capacidade de ajudar os editores a mostrarem os erros diretamente no código que o programador estiver escrevendo, além de ajudarem ele a navegar pelo código, e proverem funcionalidades de auto-complete, devido a capacidade analisar o código estáticamente enquanto ele é escrito.

Estrutural vs nominal

As linguagens mais famosas a serem associadas como "tipadas" ou "fortemente tipadas" como C ou Java, que acabaram por ditar parte do senso comum relacionado a tipagem, utilizavam o que é conhecido como sistema de tipos nominal, nesse caso, mesmo que dois valores sejam identicos, se eles pertecerem a tipos com nomes diferentes, então eles são de tipos diferentes, por exemplo, se você tiver duas classes com as mesmas propriedades e métodos no PHP, os objetos produzidos por essas classes não serão dos mesmos tipos, ex:

class Foo
{
    public function __construct(public string $property) {}

    public function method(): void
    {
        echo $this->property;
    }
}

class Bar
{
    public function __construct(public string $property) {}

    public function method(): void
    {
        echo $this->property;
    }
}

function acceptFoo(Foo $foo): void
{
    $foo->method();
}

acceptFoo(new Foo('A')); // Roda normal
acceptFoo(new Bar('A')); // Erro!
Enter fullscreen mode Exit fullscreen mode

Isso ocorre pois apesar do conteúdo desses objetos ser o mesmo, eles são de tipos com nomes diferentes, e por isso são de um sistema nominal de tipos.

O contrário do sistema nominal seria o sistema estrutural, aqui se segue a filosofia do duck typing:

Se algo anda como um pato, faz quack como um pato, então deve ser um pato

Dessa forma, um valor é considerado de um tipo, se a sua estrutura é identica a estrutura desse tipo, então no caso de linguagens como o TS, nosso exemplo anterior rodaria sem problemas:

class Foo {
  constructor(public property: string) {}

  method(): void {
    console.log(this.property);
  }
}

class Bar {
  constructor(public property: string) {}

  method(): void {
    console.log(this.property);
  }
}

function acceptFoo(foo: Foo): void {
  foo.method();
}

acceptFoo(new Foo('A')); // Ok
acceptFoo(new Bar('A')); // Ok
acceptFoo({ property: 'A', method() {} }); // Ok
Enter fullscreen mode Exit fullscreen mode

Aqui por Foo, Bar, e até o objeto literal declarado no final terem a mesma estrutura (métodos e propriedades obedecendo aos mesmos tipos), eles são considerados como do mesmo tipo, e por isso, dessa vez, a função executa sem problemas.

A comparação acaba sendo que um sistema estrutural acaba por ser mais flexível, e portanto mais amigável, enquanto um sistema nomimal acaba por ser um pouco mais rígido, embora nesse caso, ambos não serem diretamente opostos em seus prós e contras, pois existem muitas situações onde ter um sistema estrutural acaba sendo uma vantagem, geralmente em código que usa o paradigma funcional acaba por ser o caso, e existem situações onde o sistema nominal acaba por ser melhor como quando se tem valores "do mesmo tipo", mas com semântica diferente na aplicação, e por isso necessitam que o nome do tipo seja levado em consideração também, ex: moedas e números inteiros, seriam number em TS, mas possuem semânticas completamente diferentes, e não poderiam ser utilizados intercambiavelmente.

Implícito vs Explícito

Para nosso último critério podemos observar se a tipagem é explícita ou implícita, o que significa verificar se a linguagem necessita que todos os tipos sejam declarados (explícita), ou se podemos omitir a declaração deles, dessa forma deixando que a linguagem advinhe qual o tipo (implícito), funcionalidade também conhecida como inferência de tipos.

Em linguagens como o Java, antes da introdução do var, era necessário que você declarasse o tipo de tudo, variáveis, parâmetros, anotações de retorno e etc. Ex:

class Point {
  public int x;
  public int y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  Point move(Point to) {
    int x = this.x + to.x;
    int y = this.y + to.y;

    return new Point(x, y);
  }
}
Enter fullscreen mode Exit fullscreen mode

O que tem o benefício de garantir que se um valor errado foi inserido/retornado em qualquer uma dessas ocasiões, um erro vai ser desparado logo naquele ponto, além de deixar o código auto-documentado, já que os tipos estão explícitos para qualquer um que ler aquilo ler, assim já obtendo várias informações só de ler por cima o código.

O problema é que essa abordagem torna o código muito verboso, além de muitas vezes forçar com que o programador digite código que a acaba por dar a sensação de ser repetitivo, chato, ou desnecessário, uma vez que o tipo correto parece óbvio, e a linguagem até mesmo poderia advinhar qual seria o tipo nesse caso.

Sendo assim, quando uma linguagem possui suporte a inferência de tipos, ela tenta advinhar o máximo possível sobre a tipagem das coisas com base no código já escrito, e assim reforça a tipagem mesmo que o programador não tenha, de fato, tipado nada ainda, ex:

class Point {
  constructor(
    public x: number,
    public y: number
  ) {}

  move(to: Point) {
    const x = this.x + to.x;
    const y = this.y + to.y;

    return new Point(x, y);
  }
}
Enter fullscreen mode Exit fullscreen mode

Aqui, apesar de termos que tipar algumas coisas, o TypeScript tenta adivinhar boa parte dos tipos utilizados nesse código, por exemplo inferindo qual seriam os tipos das constantes x e y, além do tipo de retorno do método move.

A vantagem é uma DX bem melhor, uma vez que a linguagem trabalha para te ajudar ao invés de fazer você digitar tudo, muitas vezes sendo até um fator que faz com que pessoas que não gostem de tipagem pelo fato de considerarem uma tarefa que deixa o código muito verboso ao mesmo tempo que não tem benefícios que compensem isso, começarem a gostar das vantagens de um sistema estático, graças a implementação de um sistema onde a tipagem é implícita na maioria dos casos também.

A desvantagem seria que ao confiar demais na linguagem, algumas vezes pode se tornar mais difícil de identificar erros de tipagem, pois eles vão ocorrer em outros lugares, além de tornar a função menos documentada, apesar do código mais limpo e conciso.

Conclusão

Ao utilizarmos esses 3 critérios, conseguimos descrever muito melhor as capacidades de uma linguagem e de forma bem mais objetiva, além de poder lançar um julgamento mais adequado quanto as capacidades disponíveis em uma linguagem ou outra.

Normalmente o termo fortemente tipado é relacionado a linguagens estáticas e explicítas, enquanto o termo fracamente é relacionado a linguagens dinamicas e implícitas, mas ainda existem nuances que variam de pessoa para pessoa, então fora que cada critério discutido ao longo deste artigo pode ser misturado com os demais na mesma linguagem, portanto dificultando a classificação entre forte ou fracamente tipado, pois a linguagem pode ser mais permissiva ao ser estrutural, mas mais rigída ao também exigir tipagem explícita por exemplo.

Assim, na próxima vez que for discutir o mérito, tente descrever a tipagem de uma linguagem com esses critérios, e acredito que será uma abordagem bem mais frutífera.

Não se esqueça de compartilhar esse artigo se ele te ajudou a aprender algo novo, até a próxima e deixe nos comentários o seu tipo favorito.

Links que podem te interessar

Ass: Suporte Cansado...

Top comments (0)