DEV Community

loading...

Elixir: use, import ou require, qual a diferença?

gustavofsantos profile image Gustavo Santos ・5 min read

Não sei você, mas quando comecei a ir a fundo em Elixir, acabei descobrindo que não entendia corretamente o que algumas palavras-chave faziam.

Essas dúvidas apareceram principalmente após eu começar a estudar e usar o framework Phoenix em projetos pessoais -- todos projetos do tipo throw away. Mesmo assim, me incomodava o fato de não entender o que o framework estava tentando me dizer, parecia que existia algum tipo de mágica desnecessária lá. E eu não gosto de mágica dentro do código.

Se você está passando pelo mesmo caso, talvez esse texto te ajude. Se você está apenas de curioso lendo, que massa! Espero que você aprenda pelo menos alguma coisa nesse texto.

Diretiva import

Tudo começa com a definição do schema de algum dado no banco de dados usando o Ecto. Se você por acaso não sabe, o Phoenix usa, por padrão, o Ecto como biblioteca para trabalhar com a persistência de dados. Quando escrevemos (ou geramos) o esquema de alguma tabela, o arquivo geralmente tem essa cara:

defmodule Project.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset # << O que essa linha faz?

  schema "users" do
    field :email, :string
    field :name, :string
    field :password, :string

    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :password])
    |> validate_required([:name, :email, :password])
  end
end

No exemplo acima, a macro schema define os campos de um determinado dado que deverá ser persistido na tabela users. Não quero entrar em detalhes sobre o Ecto nesse artigo, então caso você não entenda o que o código acima faz, imagine que o trecho abaixo define as colunas e os tipos dos dados armazenados em cada coluna da tabela users:

  schema "users" do
    field :email, :string
    field :name, :string
    field :password, :string

    timestamps()
  end

Entretanto, sobre a perspectiva da diretiva import, esse trecho é irrelevante. Concentre-se em:

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :password])
    |> validate_required([:name, :email, :password])
  end

O módulo Project.Accounts.User define uma função chamada changeset que usa outras duas funções que não estão definidas neste módulo: cast e validate_required. De onde essas funções vêm?

A palavra-chave import funciona como um indicativo ao compilador: ei, quando você compilar o módulo *Project.Accounts.User, inclua todas as funções e macros definidas no módulo **Ecto.Changeset, assim o código do módulo User vai poder usar essas funções/macros*. É devido a esse comportamento que conseguimos usar as funções cast e validate_required dentro da função changeset.

Se você quiser um exemplo melhor, veja o seguinte arquivo que define dois módulos:

defmodule Module1 do
  def func do
    IO.puts "rodando Module1.func"
  end

  defmacro meu_unless(clause, do: expression) do
    # fonte: https://elixir-lang.org/getting-started/meta/macros.html
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end

defmodule Mods do
  import Module1

  def run do
    IO.puts("rodando!")
    func()
    meu_unless false, do: IO.puts "rodei"
  end
end

No módulo Mods foram importadas funções e macros de Module1 e ambos são usados na definição da função Mods.run/1. Ah, a diretiva também possui o mesmo comportamento quando usada dentro do corpo de uma função! Se a linha import Module1 for movida para dentro da definição de run, o código ainda vai funcionar. Por exemplo:

defmodule Mods do
  def run do
    import Module1
    IO.puts("rodando!")
    func()
    meu_unless false, do: IO.puts "rodei"
  end
end

Então, resumindo: a diretiva import importa todas as funções e macros de um **módulo* para o contexto onde a diretiva import está sendo usada*.

Diretiva require

Para a sua surpresa, a diretiva require faz o mesmo que a diretiva import, entretanto não importa funções! Se usarmos o mesmo exemplo anterior e trocarmos a palavra import por require, dessa forma:

defmodule Module1 do
  def func do
    IO.puts "rodando Module1.func"
  end

  defmacro meu_unless(clause, do: expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end

defmodule Mods do
  require Module1 # <<

  def run do
    IO.puts("rodando!")
    func()
    meu_unless false, do: IO.puts "rodei"
  end
end

Você verá que o compilador do Elixir vai informar o seguinte erro:

== Compilation error in file lib/mods.ex ==
** (CompileError) lib/mods.ex:18: undefined function func/0
    (elixir 1.10.4) src/elixir_locals.erl:114: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3    
    (stdlib 3.8) erl_eval.erl:680: :erl_eval.do_apply/6

Meio confuso né? Mas é basicamente o Elixir tentando nos dizer que a função func não está definida dentro do contexto onde é invocada.

Diretiva use

Voltando para o primeiro trecho de código que foi mostrado nesse artigo, o que a linha use Ecto.Schema faz? Imagine que o use consegue de alguma forma injetar um comportamento dentro do contexto.

O que é um comportamento? Como funciona essa "injeção"? Veja a definição do módulo Ecto.Schema em https://github.com/elixir-ecto/ecto/blob/master/lib/ecto/schema.ex. Pelo menos na branch master no momento que escrevo esse artigo (noite do dia 4/out/2020), na linha 450 começa a definição de uma macro chamada __using__. Por quê essa macro é importante?

A diretiva use injeta um comportamento no módulo/contexto onde é usada permitindo que o compilador do Elixir insira, edite ou remova código definido dentro do contexto corrente. Lembre-se que trabalhar com macros é como trabalhar com o compilador. Você não está escrevendo código para ser executado em tempo de execução (como todo código que você provavelmente escreve hoje), mas sim, escreve código para ser "executado" durante a compilação.

Segundo a documentação no site oficial do Elixir, o módulo Example que usa um módulo Feature, permite que Feature opere sobre o contexto de Example:

defmodule Feature do
  defmacro __using__(_) do
    quote do
      IO.puts "Feature.__using__/1"

      def some_func do
        IO.puts "oops"
      end
    end
  end
end

defmodule Example do
  use Feature
end

Quando compilado, o módulo Example terá a seguinte cara:

defmodule Example do
  require Feature
  Feature.__using__()
end

O que ganhamos com isso? A função __using__ define uma nova função some_func. Essa função será injetada dentro do contexto onde o use está sendo usado. Por exemplo:

defmodule Example do
  use Feature

  def run do
    some_func()
  end
end

Quando executada a função Example.run/1, o seguinte resultado será mostrado no terminal:

Feature.__using__/1
oops 

Conclusão

Propositalmente escolhi não entrar muito em detalhes nesse texto justamente porque o objetivo era entender a diferença entre use, import e require já que essas três palavras aparecem várias vezes em códigos Elixir, principalmente se você estiver usando Phoenix e Ecto.

Existe um mundo a parte só envolvendo o sistema de macros de Elixir, conseguimos desde injetar métodos em contextos como também estender a linguagem como bem entendermos.

O Ecto é uma prova viva. A biblioteca implementa praticamente uma linguagem dentro de Elixir para conseguir lidar de forma eficiente com queries no banco de dados.

Para saber mais

Algumas fontes que eu usei para responder a questão que envolveu a escrita desse texto, talvez seja interessante a leitura:

Discussion (0)

pic
Editor guide