Do you want to pick up from where you left of?
Take me there

Especificações e tipos

Nessa lição vamos aprender sobre a sintaxe de @spec e @type. O primeiro é mais uma sintaxe complementar para escrever documentação que pode ser analisada por ferramentas. A segunda nos ajuda a escrever código de fácil leitura e entendimento.

Introdução

Não é incomum querer descrever a interface de sua função. Claro, você pode utilizar a anotação @doc, mas isso é somente informação para outros desenvolvedores. Isso não será checado em tempo de compilação. Para isso, Elixir tem uma anotação chamada @spec para descrever especificação de função que vai ser analisada pelo compilador.

Em alguns casos, a especificação é grande e complicada. Se você quiser reduzir a complexidade, você deverá introduzir um tipo de definição personalizada. Elixir tem a anotação @type para isso. Em contra partida, Elixir é uma linguagem dinâmica. Isso significa que toda informação a respeito do tipo será ignorado pelo compilador, mas pode ser utilizada por outras ferramentas.

Especificação

Se você tem experiência com Java, você poderá pensar em especificação como uma interface. A especificação define quais os tipos de parâmetros da função e o valor de retorno.

Para definir tipos de entrada e saída, usamos a diretiva @spec localizada antes da definição da função e tomando como um params nome da função, lista de tipos de parâmetros, e depois :: tipo de valor de retorno.

Vamos ver um exemplo:

@spec sum_product(integer) :: integer
def sum_product(a) do
  [1, 2, 3]
  |> Enum.map(fn el -> el * a end)
  |> Enum.sum()
end

Depois de tudo ok, quando chamamos, o resultado válido vai ser retornado, mas a função Enum.sum retorna number não integer como era esperado em @spec. Isso pode ser uma fonte de bugs! Existem ferramentas como Dialyzer para análises estáticas de código que nos ajudam a localizar esses tipos de bugs. Vamos falar sobre eles em outra lição.

Tipos personalizados

Escrever especificações é bom, mas algumas vezes nossas funções trabalham com mais estruturas de dados complexos do que simplesmente números ou coleções. Nesses casos de definição em @spec isso poderá ser difícil de entender e/ou alterar para outros desenvolvedores. Algumas funções precisam ter um número grande de parâmetros ou retornar dados complexos. Uma longa lista de parâmetros é um de muitos problemas em potencial em um código. Em linguagens orientadas a objeto como Ruby ou Java, podemos facilmente definir classes que nos ajudam a resolver esse problema. Elixir não tem classes, mas por conta disso é fácil extender o que define nossos tipos.

Elixir contém alguns tipos básicos como integer ou pid. Você pode encontrar uma lista completa de tipos disponíveis na documentação.

Definindo tipos personalizados

Vamos modificar nossa função sum_times e inserir alguns parâmetros extras:

@spec sum_times(integer, %Examples{first: integer, last: integer}) :: integer
def sum_times(a, params) do
  for i <- params.first..params.last do
    i
  end
  |> Enum.map(fn el -> el * a end)
  |> Enum.sum()
  |> round
end

Inserimos uma estrutura no módulo Examples que contém dois campos, first e last. Essa é uma versão simples de estrutura do módulo Range. Falaremos sobre structs quando iniciarmos as discussões sobre módulos. Vamos imaginar que precisamos especificar a estrutura Examples em vários lugares. Seria chato escrever especificações longas, complexas e isso seria uma fonte de bugs. Uma solução para esse problema é @type.

Elixir tem três diretivas para tipos:

Vamos definir nosso tipo:

defmodule Examples do
  defstruct first: nil, last: nil

  @type t(first, last) :: %Examples{first: first, last: last}

  @type t :: %Examples{first: integer, last: integer}
end

Já definimos o tipo t(first, last), que é uma representação da estrutura %Examples{first: first, last: last}. Nesse ponto, vemos tipos que podem receber parâmetros, mas definimos o tipo t e nesse momento, ele é uma representação da estrutura %Examples{first: integer, last: integer}.

Qual a diferença? A primeira representa a estrutura Examples e as duas chaves poderiam receber qualquer tipo. A segunda representa a estrutura que as chaves recebem são do tipo integers. Que significa um código como este:

@spec sum_times(integer, Examples.t()) :: integer
def sum_times(a, params) do
  for i <- params.first..params.last do
    i
  end
  |> Enum.map(fn el -> el * a end)
  |> Enum.sum()
  |> round
end

É igual ao código:

@spec sum_times(integer, Examples.t(integer, integer)) :: integer
def sum_times(a, params) do
  for i <- params.first..params.last do
    i
  end
  |> Enum.map(fn el -> el * a end)
  |> Enum.sum()
  |> round
end

Documentação de tipos

O último elemento que vamos falar é sobre como documentar nossos tipos. Como vimos na documentação, temos as anotações @doc e @moduledoc para criar documentação para funções e módulos. Para documentar nossos tipos, usamos @typedoc:

defmodule Examples do
  @typedoc """
      Tipo que representa a estrutura Examples com :first como integer e :last como integer.
  """
  @type t :: %Examples{first: integer, last: integer}
end

A diretiva @typedoc é similar a @doc e @moduledoc.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!