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:
Top comments (0)