DEV Community

Cover image for Por que usar @typespecs em seu código Elixir?
Rômulo Silva
Rômulo Silva

Posted on • Edited on

Por que usar @typespecs em seu código Elixir?

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:

@spec add(integer, integer) :: integer
def add(x, y), do: x + y
Enter fullscreen mode Exit fullscreen mode

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.

@spec find_user(user_id) :: map | nil
def find_user(user_id), do: ...
Enter fullscreen mode Exit fullscreen mode

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

@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: ...
Enter fullscreen mode Exit fullscreen mode

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.

@spec get_names() :: [String.t()]
def get_names do
  ["Tomate", "Elixir", "Erlang"]
end
Enter fullscreen mode Exit fullscreen mode

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:

@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
Enter fullscreen mode Exit fullscreen mode

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

@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
Enter fullscreen mode Exit fullscreen mode

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

@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
Enter fullscreen mode Exit fullscreen mode

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

@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
Enter fullscreen mode Exit fullscreen mode

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:

@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
Enter fullscreen mode Exit fullscreen mode

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:

@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
Enter fullscreen mode Exit fullscreen mode

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
Elixir Typespecs and Behaviours
Elixir Typespecs Tutorial
Erlang Type Specifications and Dialyzer


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. 💜

Top comments (3)

Collapse
 
elixir_utfpr profile image
Elixir UTFPR (por Adolfo Neto)

Fiz um vídeo sobre este texto:

Por que usar @typespecs em seu código Elixir?, por Rômulo Silva

youtu.be/Kl5iStovPJo

Collapse
 
rohlacanna profile image
Rômulo Silva

Muito obrigado pelo vídeo professor! Anotei todos os feedbacks 💜

Collapse
 
jpramires profile image
João Pedro Ramires

Linguagem dinâmica tem dessas né, que bom que existe uma feature tão expressiva!