Por que usar @typespecs em seu código Elixir?
Tags: elixir, erlang, typespecs, tutorial
Published: 2023-02-27 14:53:28

Elixir é uma linguagem dinâmica e concisa, mas nem sempre é fácil garantir a segurança e a legibilidade do código. Felizmente, Elixir oferece uma ferramenta poderosa para ajudar a resolver esses problemas: os @typespecs.

Os @typespecs são anotações de tipo opcionais que podem ser adicionadas a funções e módulos em Elixir. Eles permitem especificar os tipos de argumentos e valores de retorno de uma função, tornando mais fácil garantir que seu código esteja correto e fácil de entender.

Neste artigo, discutiremos o que são os @typespecs, especificações e tipos, suas vantagens, desvantagens, e como eles podem ser usados para tornar o seu código Elixir mais seguro e legível.

## O que são @typespecs?

Como dito anteriormente, os @typespecs são anotações de tipo opcionais que podem ser adicionadas a funções e módulos em Elixir e usamos ​​para especificar os tipos de argumentos e valores de retorno de uma função.

Os @typespecs são escritos como comentários acima da função ou módulo que eles descrevem. Por exemplo, a especificação de tipo para uma função que adiciona dois números inteiros seria a seguinte:

```elixir
@spec add(integer, integer) :: integer
def add(x, y), do: x + y
```

Aqui, `@spec` indica que estamos adicionando uma especificação de tipo para a função **add**. O tipo de argumentos é especificado como **integer**, **integer**, e o tipo de retorno é especificado como **integer**.

## Especificações e tipos

Os @typespecs também podem ser usados ​​para especificar tipos de valores de retorno que podem ser nil. Vamos ver alguns exemplos a seguir.

```elixir
@spec find_user(user_id) :: map | nil
def find_user(user_id), do: ...
```

Aqui, especificamos que a função **find_user** recebe um argumento do tipo **user_id** e retorna um map ou **nil**.

```elixir
@type user_id :: integer
@type user :: %{id: user_id, name: String.t(), email: String.t(), age: integer}

@spec find_user(user_id) :: user | nil
def find_user(user_id), do: ...
```

Nesse exemplo, definimos dois tipos: **user_id** e **user**. O **user_id** é um alias para o tipo **integer**, que representa o ID de um usuário. O tipo **user** é um map que contém informações sobre o usuário, como o ID, o nome, o email e a idade.

Na especificação de tipo para a função **find_user**, usamos o tipo **user_id** como argumento e especificamos que a função pode retornar um map **user** ou **nil**. Isso significa que, se a função encontrar um usuário com o ID fornecido, ela retornará um map **user**. Caso contrário, ela retornará **nil**.

```elixir
@spec get_names() :: [String.t()]
def get_names do
  ["Tomate", "Elixir", "Erlang"]
end
```

Nesse exemplo, estamos especificando o tipo de retorno de uma função que retorna uma lista de strings.

Da mesma forma, você pode especificar o tipo de retorno de uma função que retorna um map da seguinte forma:

```elixir
@type user_info :: %{name: String.t(), age: integer}
@spec get_user_info(user_id :: integer) :: user_info
def get_user_info(user_id) do
  %{name: "Floki", age: 4}
end
```

Os @typespecs também podem ser usados para especificar tipos como listas de maps ou maps de listas. Por exemplo:

```elixir
@type address :: %{street: String.t(), city: String.t()}
@type person :: %{name: String.t(), age: integer, addresses: [address]}
@spec get_person() :: person
def get_person do
  %{
    name: "Floki",
    age: 4,
    addresses: [
      %{street: "123 Rua A", city: "Rio de Janeiro"},
      %{street: "456 Rua B", city: "Angra dos Reis"}
    ]
  }
end
```

Podemos também combinar para criar definições de tipos mais complexas. Por exemplo:

```elixir
@type coordinates :: {integer, integer}
@type circle :: %{center: coordinates, radius: integer}
@type rectangle :: %{top_left: coordinates, bottom_right: coordinates}
@type shape :: circle | rectangle
@spec draw_shape(shape) :: :ok
def draw_shape(shape) do
  # ...
end
```

Neste exemplo, criamos definições de tipos para coordenadas, círculos e retângulos e, em seguida, combinamos essas definições de tipos para criar um tipo de forma que pode ser um círculo ou um retângulo. A função **draw_shape** aceita qualquer tipo de forma e retorna `:ok`.

O mesmo se aplica quando queremos usar os @typespecs para especificar retornos de funções que podem resultar em erros ou mensagens personalizadas:

## Retornando erros com @typespecs 

```elixir
@spec get_user(user_id :: integer()) :: {:ok, map()} | {:error, atom()}
def get_user(user_id) do
  case Repo.get(User, user_id) do
    %User{} = user ->
      {:ok, user}
    nil ->
      {:error, :not_found}
  end
end
```

A função **get_user** retorna uma **tuple** com **:ok** e um map contendo as informações do usuário, ou uma tuple com **:error** e um átomo representando o tipo de erro ocorrido.

#### Retornando um Ecto.Changeset com @typespecs: 

```elixir
@spec create_user(user_params :: map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def create_user(user_params) do
  changeset = User.changeset(%User{}, user_params)

  case Repo.insert(changeset) do
    {:ok, user} ->
      {:ok, user}
    {:error, changeset} ->
      {:error, changeset}
  end
end
```

A função **create_user** retorna uma tuple com **:ok** e um map contendo as informações do usuário criado com sucesso, ou uma tuple com **:error** e um **Ecto.Changeset** contendo informações sobre os erros de validação ocorridos durante a criação do usuário.

#### Retornando uma mensagem personalizada com @typespecs:

```elixir
@spec validate_password(password :: String.t(), confirm_password :: String.t()) :: :ok | {:error, String.t()}
def validate_password(password, confirm_password) do
  if password == confirm_password do
    :ok
  else
    {:error, "As senhas não coincidem."}
  end
end
```
Nesse exemplo, a função **validate_password** retorna **:ok** caso as senhas informadas sejam iguais, ou uma tuple com **:error** e uma mensagem personalizada caso as senhas não coincidam.

## Vantagens dos @typespecs

Os @typespecs têm várias vantagens importantes:

- **Ajuda a garantir a segurança do seu código**: Especificando tipos de argumentos e valores de retorno com @typespecs ajudando a garantir que seu código esteja correto e seguro. Ele também ajuda a detectar erros mais cedo no processo de desenvolvimento, antes que possam se transformar em problemas maiores.

- **Torna seu código mais explícito**: Especificar tipos de argumentos e valores de retorno com @typespecs torna seu código mais fácil de entender. Isso ajuda a evitar confusão e erros causados por informações ambíguas ou mal documentadas.

- **Ajuda na documentação do código**: Os @typespecs podem ser usados como parte da documentação do seu código. Isso pode tornar sua API mais fácil de entender e usar para outros desenvolvedores que possam trabalhar em seu projeto.

- **Facilita a manutenção do código**: Os @typespecs podem ajudar a tornar a manutenção do seu código mais fácil e segura. Quando você altera uma função, pode verificar se a alteração afetou os tipos de argumentos ou valores de retorno da função e atualizar a especificação de tipo em conformidade. Isso ajuda a evitar quebras de código e problemas de integração.

## Desvantagens dos @typespecs

Embora os @typespecs tenham muitas vantagens, também existem algumas desvantagens a serem consideradas:

- **Podem adicionar complexidade ao código**: Especificar tipos de argumentos e valores de retorno pode adicionar complexidade ao código. Isso pode tornar o código mais difícil de entender ou ler para desenvolvedores que não estão familiarizados com os @typespecs.

- **Podem aumentar o tempo de desenvolvimento**: Especificar tipos de argumentos e valores de retorno pode aumentar o tempo de desenvolvimento. Isso ocorre porque você precisa escrever e atualizar as especificações de tipo à medida que escreve o código.

- **Não podem garantir 100% de segurança**: Embora os @typespecs possam ajudar a garantir a segurança do seu código, eles não podem garantir 100% de segurança. Você ainda precisa testar e verificar seu código para garantir que ele esteja correto e seguro.

## Análise de tipos com Dialyzer

Os @typespecs podem ser usados em conjunto com a ferramenta Dialyzer do Erlang para fornecer uma análise estática de tipos ainda mais poderosa e detectar erros de tipos em tempo de compilação. O Dialyzer é uma ferramenta de análise de tipo estático que pode ser usada para verificar a correção do código Elixir. Ele analisa o código-fonte e os @typespecs em busca de inconsistências e gera avisos e erros se encontrar algum problema.

Para usar o Dialyzer, você precisa instalar o pacote **dialyxir** e executar o comando **mix dialyzer**. E então o Dialyzer irá examinar o seu código e fornecer informações detalhadas sobre quaisquer problemas de tipos encontrados.

## Conclusão

Os @typespecs são uma ferramenta poderosa e valiosa para o desenvolvimento de código em Elixir. Eles ajudam a garantir a segurança e a legibilidade do código, facilitam a manutenção e a documentação do código e podem ser usados como parte da documentação da API.

Embora os @typespecs possam adicionar complexidade ao código e aumentar o tempo de desenvolvimento, os benefícios superam as desvantagens. Se você está desenvolvendo código em Elixir, é altamente recomendável considerar o uso de @typespecs em suas funções e módulos.

## Referências

[Elixir Typespecs](https://hexdocs.pm/elixir/main/typespecs.html)
[Elixir Typespecs and Behaviours](https://elixir-lang.org/getting-started/typespecs-and-behaviours.html)
[Elixir Typespecs Tutorial](https://www.tutorialspoint.com/elixir/elixir_typespecs.htm)
[Erlang Type Specifications and Dialyzer](erlang.org/doc/reference_manual/typespec.html)

---

Muito obrigado pela leitura até aqui e espero ter ajudado de alguma forma. Tem alguma sugestão ou encontrou algum problema? por favor deixe-me saber. 💜