Code

Elixir – 08: Modules

February 16, 2016

author:

Elixir – 08: Modules

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

Elixir – 08: Modules

Elixir에서는 여러 함수들을 모듈을 통해서 묶습니다. 이미 이전 장에서도 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

다음 절부터는 코드가 점점 길어지기 때문에 쉘에 직접 타이핑하는 것이 귀찮아질겁니다. 이제 슬슬 Elixir 코드를 컴파일 하고, 실해하는 방법에 대해서 배워야 할 때죠.

Compilation

모듈을 작성해서 파일에 저장하는 것은 많은 경우에 유리합니다. 다음과 같은 내용을 가지는 math.ex 모듈을 생각해보죠.

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

이 파일은 elixirc를 사용해서 컴파일할 수 있습니다.

$ elixirc math.ex

정의된 모듈을 바이트 코드 형태로 저장하고 있는 Elixir.Math.beam 파일이 생성됩니다. 이제 다시 iex를 실행하면, 이 모듈에 정의된 함수를 사용할 수 있습니다. iex는 자신이 실행된 폴더에 존재하는 바이트 코드를 자동으로 읽어줍니다. 🙂

iex> Math.sum(1, 2)
3

Elixir 프로젝트는 보통 다음과 같은 폴더 3개로 구성됩니다.

  • ebin – 컴파일된 바이트 코드들
  • lib – elixir 코드(.ex)
  • test – 테스트 코드(.exs)

실제 프로젝트를 진행할 때에는 mix라고 불리는 빌드 툴이 컴파일과 패스 설정을 담당합니다. 뿐만 아니라, 학습의 편의를 위해서 컴파일을 하지 않는, 좀 더 유연한 스크립트 모드도 제공합니다.

스크립트 모드(Scripted mode)

파일 확장자 .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

파일은 메모리 상에서 컴파일되고 실행되며, “3”을 출력합니다. 앞으로는 스크립트 파일로 코드를 저장하고 실행하기를 권장합니다.

Named functions

모듈 내부에서는 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)

함수 선언에서도 가드와 중복 선언이 가능합니다. 함수가 여러개의 선언을 가지고 있다면 Elixir는 매칭하는 선언을 찾을 때까지 탐색합니다. 다음은 주어진 숫자가 0인지 아닌지를 확인하는 함수의 구현입니다.

defmodule Math do
  def zero?(0) do
    true
  end

  def zero?(x) when is_number(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)

주어진 매개변수와 매칭되는 선언이 없다면 에러를 던집니다.

if와 비슷하게, 이름 있는 함수는 이전에도 배웠던 do:do/end 블럭 문법을 사용할 수 있습니다. 이를 통해 위 예제를 다음과 같이 쓸 수도 있죠.

defmodule Math do
  def zero?(0), do: true
  def zero?(x) when is_number(x), do: false
end

많이 짧아졌고, 여전히 동일한 동작을 제공합니다. 한 줄짜리 코드라면 do:를 쓸 일이 많겠지만, 아니라면 do/end를 쓸 경우가 대부분일겁니다.

Function capturing

이 튜토리얼을 진행하는 동안 우리는 이름/차수 형식을 통해 함수를 표현했습니다. 이는 단순히 문서상의 표현이 아닙니다. iex를 켜고, 위에서 만든 meth.exs 파일을 실행합시다.

$ iex math.exs
iex> Math.zero?(0)
true
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function(fun)
true
iex> fun.(0)
true

is_function/1처럼 로드된 함수는 모듈 없이 캡쳐할 수 있습니다.

iex> &is_function/1
&:erlang.is_function/1
iex> (&is_function/1).(fun)
true

캡쳐 문법에서도 여전히 함수 생성을 위해서 짧은 표현을 사용할 수 있다는 점은 중요합니다.

iex> fun = &(&1 + 1)
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> fun.(1)
2

여기에서 &1는 함수에 넘겨진 첫번째 매개변수를 가리킵니다. 그러므로 &(&1+1)fn x -> x + 1 end와 동일합니다. 위에 보인 문법은 짧은 함수 선언에서 무척 유용합니다.

만약 모듈에 속해있는 함수를 호출하는 경우에는 &Module.function()와 같은 형식을 사용하세요.

iex> fun = &List.flatten(&1, &2)
&List.flatten/2
iex> fun.([1, [[2], 3]], [4, 5])
[1, 2, 3, 4, 5]

&List.flatten(&1, &2)fn(list, tail) -> List.flatten(list, tail) end 라고도 작성할 수 있으며, 이는 &List.flatten/2와도 동일합니다. 캡쳐 연산자 &에 대한 더 자세한 설명은 Kernel.SpecialForms 문서를 참고하세요.

Default arguments

Elixir에서 이름 있는 함수는 매개변수의 기본값을 지원합니다.

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 \ IO.puts "hello") do
    x
  end
end
iex> DefaultTest.dowork
hello
:ok
iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
hello
:ok

보면 기본값을 사용해야하는 경우에만 평가되는 것을 확인할 수 있습니다.

만약 기본값을 사용하는 함수 선언을 여러 개 사용하고 싶다면, 함수 머리(실제 내용물을 포함하지 않는 선언)를 사용하고, 그곳에서 기본값을 지정해야 합니다.

defmodule Concat do
  def join(a, b \ nil, sep \ " ")

  def join(a, b, _sep) when is_nil(b) do
    a
  end

  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
IO.puts Concat.join("Hello")               #=> Hello

기본값을 사용할 때에는 함수 정의를 덮어쓰지 않도록 주의하세요. 예를 들어 다음과 같은 상황이 있을 수 있습니다.

defmodule Concat do
  def join(a, b) do
    IO.puts "***First join"
    a <> b
  end

  def join(a, b, sep \ " ") do
    IO.puts "***Second join"
    a <> sep <> b
  end
end

이러한 코드를 컴파일 하게 되면 Elixir는 다음과 같은 경고를 던져줄 겁니다.

concat.ex:7: this clause cannot match because a previous clause at line 2 always matches

컴파일러는 매개변수 2개와 함께 join을 호출하면 언제나 첫번째 join에 매칭될 것이라는 사실을 알려줍니다.

$ iex concat.exs
iex> Concat.join "Hello", "world"
***First join
"Helloworld"
iex> Concat.join "Hello", "world", "_"
***Second join
"Hello_world"

Consideration

의문은 깊어져가고…

  • def와 fn의 차이는 무엇인가
  • 매크로와 함수의 차이는 무엇인가
  • 여긴 어디고 나는 누구….

Reference