8 모듈 - Modules

Elixir는 모듈안에 여러 함수를 그룹화합니다. 이전 장에서 이미 the String module와 같은 여러 모듈을 사용해 왔습니다:

In Elixir we group several functions into modules. We've already used many different modules in the previous chapters like the String module:

iex> String.length "hello"
5

모듈을 생성하기 위해서는 defmodule 매크로를 사용합니다. 모듈안에 함수를 정의하기 위해 def 매크로를 사용합니다.

In order to create our own modules in Elixir, we use the defmodule macro. We use the def macro to define functions in that module:

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

iex> Math.sum(1, 2)
3

다음 절에서는 예제가 좀 복잡해서 쉘에서 입력하려면 주의해야합니다. 이 참에 어떻게 Elixir 코드를 컴파일하거나 어떻게 Elixir 스크립트를 실행하는지 배워 봅시다.

In the following sections, our examples are going to get a bit more complex, and it can be tricky to type them all in the shell. It's about time for us to learn how to compile Elixir code and also how to run Elixir scripts.

8.1 컴파일 - Compilation

모듈을 파일에 코딩하는것은 컴파일하거나 재사용 할 수 있기 때문에 편리합니다. math.ex 파일에 아래 내용을 코딩해봅시다:

Most of the time it is convenient to write modules into files so they can be compiled and reused. Let's assume we have a file named math.ex with the following contents:

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

elixirc을 사용하여 컴파일 해봅시다:

This file can be compiled using elixirc:

$ elixirc math.ex

컴파일러는 바이트코드를 포함하고 있는 Elixir.Math.beam 파일을 만듭니다. 만약 iex를 다시 시작하면 작성했던 모듈 코드를 사용할 수 있습니다(단, 모듈파일과 같은 디렉토리에서 iex를 실행해야 합니다):

This will generate a file named Elixir.Math.beam containing the bytecode for the defined module. If we start iex again, our module definition will be available (provided that iex is started in the same directory the bytecode file is in):

iex> Math.sum(1, 2)
3

Elixir 프로젝트는 대부분 3개의 디렉토리로 구성됩니다.

Elixir projects are usually organized into three directories:

  • ebin - 컴파일된 바이트코드를 포함함
  • lib - elixir 코드를 포함함(대부분 .ex 파일들)
  • test - 테스트 파일들을 포함함(대부분 .exs 파일들)

  • ebin - contains the compiled bytecode

  • lib - contains elixir code (usually .ex files)

  • test - contains tests (usually .exs files)

실제 프로젝트에서는 mix 라는 빌드 툴이 컴파일 및 적절한 경로를 세팅 해줍니다. 학습 목적을 위해서 Elixir는 컴파일 결과물을 만들지 않고 좀 더 유연한 스크립트 모드를 지원합니다.

When working on actual projects, the build tool called mix will be responsible for compiling and setting up the proper paths for you. For learning purposes, Elixir also supports a scripted mode which is more flexible and does not generate any compiled artifacts.

8.2 스크립트 모드 - Scripted mode

.ex 확장자 말고도 스크립트용으로 .exs 확장자를 지원합니다. Elixir는 둘다 동일하게 처리하지만 차이라곤 .ex 파일은 컴파일되는 반면에 .exs 파일은 컴파일없이 스크립트용으로 사용됩니다. 예를들어 math.exs 파일을 만들수 있습니다:

In addition to the Elixir file extension .ex, Elixir also supports .exs files for scripting. Elixir treats both files exactly the same way, the only difference is in intention. .ex files are meant to be compiled while .exs files are used for scripting, without the need for compilation. For instance, we can create a file called math.exs:

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

IO.puts Math.sum(1, 2)

실행해보면:

And execute it as:

$ elixir math.exs

이 파일은 메모리에서 컴파일되고 실행되고 "3"을 결과로 출력합니다. 어떤 바이트코드도 만들어지지 않습니다. 앞으로 예제는 스크립트 파일에 코딩할 것을 추천합니다.

The file will be compiled in memory and executed, printing "3" as the result. No bytecode file will be created. In the following examples, we recommend you write your code into script files and execute them as shown above.

8.3 명명(Named) 함수 - Named functions

모듈속에서 def/2로 함수를 정의하고 defp/2로 private 함수를 정의합니다. def/2로 정된 함수는 외부모듈에서 호출 될수 있고 private 함수는 모듈안에서만 호출할 수 있습니다.

Inside a module, we can define functions with def/2 and private functions with defp/2. A function defined with def/2 can be invoked from other modules while a private function can only be invoked locally.

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

  defp do_sum(a, b) do
    a + b
  end
end

Math.sum(1, 2)    #=> 3
Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

함수선언은 가드와 다중(multiple)절을 지원합니다. 만약 함수가 여러절을 가지고 있으면 각 절이 매칭될때까지 계속 확인합니다. 인수가 0인지 아닌지 확인하는 코드는 아래와 같습니다:

Function declarations also support guards and multiple clauses. If a function has several clauses, Elixir will try each clause until it finds one that matches. Here is an implementation of a function that checks if the given number is zero or not:

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

  def zero?(x) when is_number(x) do
    false
  end
end

Math.zero?(0)  #=> true
Math.zero?(1)  #=> false

Math.zero?([1,2,3])
#=> ** (FunctionClauseError)

인자가 어느 절과도 일치하지 않으면 오류가 발생합니다.

Giving an argument that does not match any of the clauses raises an error.

8.4 함수 캡쳐링(capturing) - Function capturing

튜토리얼을 진행하면서 함수를 참고하기 위해 함수이름/인자의개수 표기법을 사용해 왔습니다. 이러한 표기법은 명명(Named) 함수를 검색하는데도 사용할 수 있습니다. iex 로 위에서 정의한 math.exs 실행시켜 봅시다:

Throughout this tutorial, we have been using the notation name/arity to refer to functions. It happens that this notation can actually be used to retrieve a named function as a function type. Let's start iex and run the math.exs file defined above:

$ 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와 같은 로컬함수, 임포트(imported) 된 함수는 모듈명없이 캡쳐링 할 수 있습니다:

Local or imported functions, like is_function/1, can be captured without the module:

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

캡쳐 문법은 함수를 만들때 단축키로 사용할 수 있습니다:

Note the capture syntax can also be used as a shortcut for creating functions:

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 와 같습니다. 이 문법은 짧은 함수를 정의할때 편리합니다. 캡처 연산자 & 대해서는 the Kernel.SpecialForms documentation에서 자세히 볼 수 있습니다.

The &1 represents the first argument passed into the function. &(&1+1) above is exactly the same as fn x -> x + 1 end. The syntax above is useful for short function definitions. You can read more about the capture operator & in the Kernel.SpecialForms documentation.

8.5 기본 인자 - Default arguments

명명(Named) 함수는 기본 인수를 사용할 수 있습니다:

Named functions in Elixir also support default arguments:

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

기본값으로 어떤 표현식도 허용하지만, 함수 정의의 경우는 평가되지 않습니다; 나중에 사용하기 위해 단순히 저장만합니다. 함수를 호출하고 함수의 기본값이 이용 될 때마다 기본값식이 평가(evaluated) 됩니다.

Any expression is allowed to serve as a default value, but it won't be evaluated during the function definition; it will simply be stored for later use. Every time the function is invoked and any of its default values have to be used, the expression for that default value will be evaluated:

defmodule DefaultTest do
  def dowork(x \\ IO.puts "hello") do
    x
  end
end
iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
hello
:ok

만약 함수의 기본값에 다중(multiple)절이 있는 경우, 기본값 선언만 하는 함수 헤드(head) 부분만 만드는 것을 권장합니다:

If a function with default values has multiple clauses, it is recommended to create a function head (without an actual body), just for declaring defaults:

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

기본값을 사용할때, 함수정의를 덮어쓰는(overlapping)것에 대해서만은 주의해야합니다. 다음의 예를 생각해 봅시다:

When using default values, one must be careful to avoid overlapping function definitions. Consider the following example:

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

위의 코드를 "concat.ex"라는 파일명으로 저장하고 컴파일하면 Elixir는 다음과 같은 경고를 출력 할 것입니다:

If we save the code above in a file named "concat.ex" and compile it, Elixir will emit the following warning:

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

2인자를 가지는 join 함수호출은 항상 첫 번째 join 정의를 호출하며, 두 번째 것은 3개의 인자가 전달 된 경우에만 호출할 수 있음을 컴파일러가 알려줍니다:

The compiler is telling us that invoking the join function with two arguments will always choose the first definition of join whereas the second one will only be invoked when three arguments are passed:

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

모듈의 간단한 소개는 여기까지 입니다. 다음 장에서는 명명(Named)함수를 어떻게 재귀로 사용하면 좋은지 배울것이며 다른 모듈에서 함수를 임포트(import)하는 데 사용되는 어휘 지시문(directives)를 살펴보고 모듈의 속성에 대해 논의할것입니다.

This finishes our short introduction to modules. In the next chapters, we will learn how to use named functions for recursion, explore Elixir lexical directives that can be used for importing functions from other modules and discuss module attributes.