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
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: ...
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: ...
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
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
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
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
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
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
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
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)
Fiz um vídeo sobre este texto:
Por que usar @typespecs em seu código Elixir?, por Rômulo Silva
youtu.be/Kl5iStovPJo
Muito obrigado pelo vídeo professor! Anotei todos os feedbacks 💜
Linguagem dinâmica tem dessas né, que bom que existe uma feature tão expressiva!