Code

Elixir – 13: alias, require and import

March 2, 2016

author:

Array

Elixir – 13: alias, require and import

Elixir Tutorial 시리즈입니다. 거의 대부분은 튜토리얼의 한글 번역에 가깝습니다만, 생략되거나 추가로 주석을 달거나 하는 부분이 많습니다. 원문은 최하단의 링크를 참고하세요.

Elixir – 13: alias, require and import

소프트웨어 재활용을 용이하게 만들기 위해서 Elixir는 3개의 디렉티브를 제공하고 있습니다. 지금부터 보게 될 그것들은 논리적인 스코프를 가지고 있기 때문에 디렉티브라고 불리고 있습니다.

alias

alias 는 주어진 모듈 이름의 앨리아스를 만듭니다. 예를 들어 Math 모듈이 어떤 이유가 있어서 특별한 리스트 구현을 사용하고 있다고 가정해봅시다.

defmodule Math do
  alias Math.List, as: List
end

이 시점부터 모든 List는 자동적으로 Math.List를 가리킨다고 여겨집니다. 만약 원래의 List에 접근하고 싶은 경우에는 Elixir. 접두사를 사용하면 됩니다.

List.flatten             #=> uses Math.List.flatten
Elixir.List.flatten      #=> uses List.flatten
Elixir.Math.List.flatten #=> uses Math.List.flatten

사실 Elixir에 정의되어 있는 모든 모듈들은 Elixir라는 네임스페이스에 정의되어 있습니다. 다만 편의를 위해서 “Elixir.”를 생략할 수 있는 것입니다.

앨리아스는 짧은 표현을 위해서 빈번하게 사용되는 방식입니다. alias:as 옵션 없이 사용하게 되면 첫번째 매개변수의 마지막 모듈명을 사용합니다. 예를 보시죠.

alias Math.List

이는 다음과 동일합니다.

alias Math.List, as: List

alias논리적인 스코프를 가진다는 점을 기억하세요. 다시 말해서 특정 함수에서만 이러한 앨리아스를 사용할 수 있습니다.

defmodule Math do
  def plus(a, b) do
    alias Math.List
    # ...
  end

  def minus(a, b) do
    # ...
  end
end

이 예제에서는 plus/2에서만 alias를 호출하고 있기 때문에 이 선언은 plus/2 함수 내부에서만 유효하며, minus/2에서는 영향을 받지 않습니다.

require

Elixir는 메타 프로그래밍을 위해서 매크로를 제공합니다.

매크로는 컴파일 시간에 확장되고 실행되는 코드 덩어리입니다. 그러므로 매크로를 사용하기 위해서는 모듈과 그 구현을 컴파일 시점에 사용할 수 있도록 보장해주어야 합니다. 이는 require 디렉티브를 통해서 실현해줄 수 있습니다.

iex> Integer.is_odd(3)
** (CompileError) iex:1: you must require Integer before invoking the macro Integer.is_odd/1
iex> require Integer
nil
iex> Integer.is_odd(3)
true

Elixir에서는 가드를 사용한 표현을 위해서 Integer.is_odd/1가 매크로로 정의되어 있습니다. 이는 Integer.is_odd/1를 호출하려면 우선 Integer 모듈을 require 해야한다는 의미와도 같습니다.

일반적으로 모듈 내에 있는 매크로를 사용하려는 목적이 아니라면, 사용하기 전에 require할 필요가 없습니다. 로드되지 않은 매크로를 사용하게 되면 에러가 발생합니다. alias 디렉티브와 마찬가지로 require는 논리적인 스코프에서 동작합니다.

import

다른 모듈에서 쉽게 함수나 매크로에 접근하기 위해서 import를 사용합니다. 예를 들어, List 모듈의 duplicate/2 함수를 사용하고 싶다면 다음과 같이 작성할 수 있습니다.

iex> import List, only: [duplicate: 2]
nil
iex> duplicate :ok, 3
[:ok, :ok, :ok]

이 경우에는 Listduplicate/2만을 가져왔습니다. :only는 선택적이지만, 지정된 모듈의 모든 함수를 네임스페이스로 끌어오는 것을 피하기 위해서 권장됩니다. 옵션으로 :except도 사용할 수 있습니다.

import:macros:functions과 같은 옵션도 지우너합니다. 예를 들어 모든 매크로를 가져오고 싶다면,

import Integer, only: :macros

이렇게 작성할 수 있으며, 모든 함수를 가져오고 싶다면 다음과 같이 작성할 수 있습니다.

import Integer, only: :functions

import 역시 논리적인 스코프에서 사용된다는 점을 기억하세요.

defmodule Math do
  def some_function do
    import List, only: [duplicate: 2]
    duplicate(:ok, 10)
  end
end

위의 예제에서는 가져온 List.duplicate/2 함수가 some_function에서만 보이게 됩니다. 다른 함수에서는 duplicate/2에 접근 할 수 없습니다.

마지막으로 import한 모듈은 자동적으로 require된다는 점도 기억해주세요.

Aliases

이 시점에서 Elixir의 앨리아스가 정확히 무엇인지, 무엇을 나타내는지 궁금할 수도 있을 것입니다.

Elixir의 앨리아스는 컴파일 시간에 첫 글자를 대문자로 만든 뒤 아톰으로 변환된 식별자입니다. 예를 들어 String 앨리아스는 :"Elixir.String"로 변환됩니다.

iex> is_atom(String)
true
iex> to_string(String)
"Elixir.String"
iex> :"Elixir.String" == String
true

alias/2 디렉티브를 통해서 아톰을 간단하게 앨리아스로 확장할 수 있습니다.

Erlang VM, 그리고 Elixir에서 모듈은 언제나 아톰으로 표현되기 때문에 앨리아스는 아톰으로 확장됩니다. 예를 들어 다음과 같은 방식으로 Erlang 모듈을 사용할 수 있습니다.

iex> :lists.flatten([1, [2], 3])
[1, 2, 3]

이는 모듈에 있는 어떤 함수를 동적으로 호출할 수 있는 기법이기도 합니다.

iex> mod = :lists
:lists
iex> mod.flatten([1, [2], 3])
[1, 2, 3]

여기에서는 아톰 :lists에 있는 flatten함수를 호출하고 있습니다.

중첩

앨리아스에 이어서 이제 중첩과 이것이 어떤식으로 동작하는 지에 대해서 알아볼 시간이 되었습니다. 다음 예제를 생각해보죠.

defmodule Foo do
  defmodule Bar do
  end
end

이 예제는 2개의 모듈(FooFoo.Bar)을 정의합니다. 두번째는 Foo의 범위 내에서는 Bar로 접근할 수 있으며, 다음과 같이 선언할 수도 있습니다.

defmodule Elixir.Foo do
  defmodule Elixir.Foo.Bar do
  end
  alias Elixir.Foo.Bar, as: Bar
end

나중에 Bar 모듈이 Foo 선언 외부로 노출된다면 그 전체 이름으로 접근하거나 위에서 보여주었던 것처럼 alias 디렉티브를 통해서 앨리아스를 설정해야할 것입니다.

Elixir에서는 모든 모듈 이름을 아톰으로서 번역하여 취급하기 때문에 FooFoo.Bar 모듈 선언 전에 미리 정의할 필요가 없이, 체인 형태로 모듈을 정의하지 않고 임의로 중첩된 모둘을 만들 수 있습니다. 예를 들어 FooFoo.Bar 선언 없이 Foo.Bar.Baz를 선언해도 좋습니다.

나중에 다시 보겠지만 앨리아스는 매크로에 있어서 무척 중요한 역할을 합니다.

Multi alias/import/require

Elixir v1.2 부터 여러개의 모듈을 한번에 앨리아싱하거나 import하거나 require할 수 있게 되었습니다. 이는 어떤 모듈을 작성하기 시작할 때에 무척 유용합니다. 예를 들어 MyApp이라는 모듈에 모든 모듈을 중첩시키려고 한다면 MyApp.FooMyApp.Bar, MyApp.Baz를 다음과 같은 구문으로 한번에 처리할 수 있습니다.

alias MyApp.{Foo, Bar, Baz}

use

use는 디렉티브는 아니지만, 현재의 컨텍스트에서 모듈을 사용할 수 있게 해주는 require와 밀접한 연관이 있는 매크로입니다. use 매크로는 다른 외부의 기능을 현재의 논리 스코프에 가져오고 싶은 경우에 사용할 수 있습니다.

예를 들어 ExUnit 프레임워크를 사용해서 테스트를 작성하기 위해서, 개발자는 ExUnit.Case 모듈을 사용해야합니다.

defmodule AssertionTest do
  use ExUnit.Case, async: true

  test "always pass" do
    assert true
  end
end

이 뒤에서는 use가 주어진 모듈을 require 하며 그 뒤에서 __using__/1 콜백을 호출하여 현재의 컨텍스트에 코드들을 주입합니다. 코드로 표현하자면 다음과 같습니다.

defmodule Example do
  use Feature, option: :value
end

는 다음과 같이 컴파일됩니다.

defmodule Example do
  require Feature
  Feature.__using__(option: :value)
end

Consideration

alias는 명료하고, require는 특정 모듈을 현재의 컨텍스트에 가져오고, import는 같은 스코프에 모듈 내에 지정된 것들을 가져온다, 정도로 정리할 수 있을텐데, 그렇다면 use 매크로는 무슨 목적으로 사용하는가? 가 아직 잘 이해되지 않는다고 할까… 설명만을 봐서는 import와 동등한 동작을 하는 것처럼 보이는데 ;ㅅ;

그래서 좀 찾아봤습니다. Link 여기에 대한 의문을 가진 사람이 꽤 있었던 모양인지 걸리는 검색이 많았음. 최종적으로는 core 메일링 리스트에 있는 길고 길고 기이이인 토론을 봤는데, 그럭저럭 납득이 가는 설명.
이해한 내용을 요약하면,

  • require는 Erlang VM에게 사용하는 함수들이 전부 매크로라는 것을 알려주기 위해서 필요.
  • import는 특정 모듈을 현재 컨텍스트로 가져오기 위해서 사용.
  • use는 require 후에 어떤 초기화 작업이 필요한 경우에 사용.

Reference

Array