모듈과 함수: 코드를 조직화하고 재사용하기
모듈과 함수: 코드를 조직화하고 재사용하기
엘릭서는 여러 함수를 **모듈(module)**로 묶어요. 이미 이전 장들에서 String 같은 다양한 모듈을 써봤죠. 이제 defmodule로 우리만의 모듈을 만들고, def·defp로 함수를 정의하는 법, 가드(guard)와 여러 절(clause), 기본 인자, 그리고 별칭(alias)과 모듈 중첩까지 차근차근 살펴볼게요.
본문
엘릭서에서 여러 함수를 모듈로 묶어요. 이전 장에서 String 모듈 같은 걸 이미 많이 써봤죠.
iex> String.length("hello")
5
우리만의 모듈을 만들려면 defmodule 매크로를 써요. 모듈 이름(아래에서 설명할 별칭)의 첫 글자는 반드시 대문자여야 해요. 그 모듈 안에서 함수를 정의할 때는 def 매크로를 써요. 모든 함수의 첫 글자는 소문자(또는 밑줄)여야 하죠.
iex> defmodule Math do
...> def sum(a, b) do
...> a + b
...> end
...> end
iex> Math.sum(1, 2)
3
이 장에서는 복잡도가 다른 우리만의 모듈을 정의할 거예요. 예시가 점점 길어지면 셸에 전부 입력하기 어려우니, 스크립트를 더 자주 쓰게 될 거예요.
스크립팅
엘릭서에는 .ex(Elixir)와 .exs(Elixir 스크립트) 두 가지 파일 확장자가 있어요. 엘릭서는 두 파일을 완전히 똑같이 취급하고, 차이는 의도뿐이에요. .ex 파일은 컴파일 대상, .exs 파일은 스크립트용이에요.
math.exs라는 파일을 만들어 볼게요.
defmodule Math do
def sum(a, b) do
a + b
end
end
IO.puts Math.sum(1, 2)
그리고 이렇게 실행해요.
$ elixir math.exs
iex 안에서 파일을 불러올 수도 있어요.
$ iex math.exs
그러면 Math 모듈에 바로 접근할 수 있어요.
함수 정의
모듈 안에서 함수는 def/2로, 비공개 함수는 defp/2로 정의해요. def/2로 정의한 함수는 다른 모듈에서 호출할 수 있지만, 비공개 함수는 같은 곳에서만 호출할 수 있어요.
defmodule Math do
def sum(a, b) do
do_sum(a, b)
end
defp do_sum(a, b) do
a + b
end
end
IO.puts Math.sum(1, 2) #=> 3
IO.puts Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)
함수 선언은 가드와 여러 절(clause)도 지원해요. 함수에 여러 절이 있으면 엘릭서는 매칭되는 절을 찾을 때까지 각 절을 시도해요. 주어진 숫자가 0인지 아닌지 확인하는 함수를 구현해 볼게요.
defmodule Math do
def zero?(0) do
true
end
def zero?(x) when is_integer(x) do
false
end
end
IO.puts Math.zero?(0) #=> true
IO.puts Math.zero?(1) #=> false
IO.puts Math.zero?([1, 2, 3]) #=> ** (FunctionClauseError)
IO.puts Math.zero?(0.0) #=> ** (FunctionClauseError)
zero? 끝의 물음표는 이 함수가 불리언을 돌려준다는 뜻이에요. 모듈·함수 이름과 변수 등의 명명 규칙에 대해 더 알고 싶다면 Naming Conventions를 보세요.
아무 절에도 매칭되지 않는 인자를 주면 오류가 발생해요.
if처럼 함수 정의도 do:와 do-블록 문법을 둘 다 지원해요. math.exs를 이렇게 고쳐도 같은 동작을 해요.
defmodule Math do
def zero?(0), do: true
def zero?(x) when is_integer(x), do: false
end
한 줄짜리 함수에는 do:를, 여러 줄에 걸친 함수에는 항상 do-블록을 쓰는 편이 좋아요. 일관성을 원한다면 코드베이스 전체에서 do-블록만 써도 돼요.
기본 인자
엘릭서의 함수 정의는 기본 인자(default argument)도 지원해요.
defmodule Concat do
def join(a, b, sep \\ " ") do
a <> sep <> b
end
end
IO.puts(Concat.join("Hello", "world")) #=> Hello world
IO.puts(Concat.join("Hello", "world", "_")) #=> Hello_world
어떤 표현식이든 기본 값으로 쓸 수 있어요. 다만 함수 정의 시점에는 평가되지 않아요. 기본 값을 사용해야 하는 순간에만 매 호출마다 그 기본 값 표현식이 평가되죠.
defmodule DefaultTest do
def dowork(x \\ "hello") do
x
end
end
iex> DefaultTest.dowork()
"hello"
iex> DefaultTest.dowork(123)
123
iex> DefaultTest.dowork()
"hello"
기본 값이 있는 함수에 여러 절이 있다면, 기본 값을 선언하기 위해 함수 헤드(function head)(본문 없이 def만 쓰는 정의)를 만들어야 해요.
defmodule Concat do
# A function head declaring defaults
def join(a, b, sep \\ " ")
# The separator is unused in this case, so we prefix it with underscore
def join(a, b, _sep) when b == "" do
a
end
def join(a, b, sep) do
a <> sep <> b
end
end
IO.puts(Concat.join("Hello", "")) #=> Hello
IO.puts(Concat.join("Hello", "world")) #=> Hello world
IO.puts(Concat.join("Hello", "world", "_")) #=> Hello_world
함수 헤드에는 패턴이나 가드를 쓸 수 없어요. 인자 이름과 기본 값만 정의할 수 있어요.
별칭(alias) 이해하기
엘릭서에서 별칭은 대문자로 시작하는 식별자(String, Keyword 등)예요. 컴파일 중에 원자(atom)로 변환되죠. 예를 들어 String 별칭은 기본적으로 :"Elixir.String"이라는 원자로 해석돼요.
iex> is_atom(String)
true
iex> to_string(String)
"Elixir.String"
iex> :"Elixir.String" == String
true
alias/2 지시어로 별칭이 확장되는 원자를 바꿀 수 있어요.
별칭이 원자로 확장되는 이유는, Erlang 가상 머신(그래서 엘릭서도)에서 모듈이 항상 원자로 표현되기 때문이에요. 그 원자들에 네임스페이스를 매김으로써 엘릭서 모듈은 기존 Erlang 모듈과 이름이 충돌하지 않아요.
iex> List.flatten([1, [2], 3])
[1, 2, 3]
iex> :"Elixir.List".flatten([1, [2], 3])
[1, 2, 3]
Erlang 모듈을 호출할 때도 같은 메커니즘이 동작해요.
iex> :lists.flatten([1, [2], 3])
[1, 2, 3]
모듈 중첩
이제 별칭을 알았으니 중첩이 어떻게 동작하는지 볼게요. 다음 예시를 봐주세요.
defmodule Foo do
defmodule Bar do
end
end
위 예시는 Foo와 Foo.Bar 두 개의 모듈을 정의해요. 두 번째 모듈은 같은 어휘 범위 안에 있으면 Foo 안에서 Bar로 접근할 수 있어요.
모듈 이름은 서로 독립적이에요 {: .info}
Foo.Bar모듈을 정의하기 전에Foo모듈을 정의할 필요는 없어요. 서로 독립적이거든요.
이후 Bar 모듈을 Foo 정의 밖으로 옮기면, 전체 이름(Foo.Bar)으로 참조하거나 alias 지시어로 별칭을 정해야 해요. 이건 다음 장에서 배울 거예요.
defmodule Foo.Bar do
end
defmodule Foo do
alias Foo.Bar
# Can still access it as `Bar`
end