Skip to content

Latest commit

 

History

History
477 lines (303 loc) · 21.5 KB

Modules and Functions: Part 1 (#10).livemd

File metadata and controls

477 lines (303 loc) · 21.5 KB

Modules and Functions: Part 1 (#10)

도입부

엘릭서 모듈과 함수에 대해 알아봅시다.

엘릭서의 함수는 예상대로 매개변수와 반환 값을 사용할 수 있지만, 엘릭서는 함수형 언어이므로 함수를 중심으로 언어가 구성되어 있습니다.

엘릭서의 일반적인 함수는 짧고 단순하며 쉽게 테스트할 수 있습니다. 그런 단순한 함수가 서로 결합하여 다른 함수를 만드는 식으로 구성됩니다. 함수를 작고 단순하게 유지하고 다른 작고 단순한 함수를 호출함으로써 엘릭서 프로그램은 이해하기 쉽고 (그리고 마찬가지로 중요한 것은) 테스트를 작성하기 쉬운 경향이 있습니다.

함수는 일급 시민으로, 다른 데이터 변수와 마찬가지로 다른 함수에 전달하고 반환할 수 있습니다. 자바스크립트에는 처음부터 일급 함수의 개념이 있었지만, 변수에 함수를 할당하고 함수를 전달하고 반환하는 것은 최근에 C#에 추가된 기능입니다.

C#에서 함수 매개변수는 다른 모든 것과 마찬가지로 항상 정적으로 타입이 지정되며, 자바스크립트에서는 매개변수와 연결된 타입이 없습니다. 엘릭서 함수 매개변수는 자바스크립트처럼 동적으로 타입이 지정되며, 각 매개변수의 타입은 코드에 선언되지 않습니다.

함수는 일반적으로 모듈에 존재하며, 이것이 엘릭서에서 함수가 그룹화되는 방식입니다.

엘릭서 소스 파일

여기서부터는 .exs 와 .ex 로 끝나는 엘릭서 소스 코드 파일을 사용하기 시작합니다. 현재 저는 .ex 엘릭서 소스 코드 파일을 빌드하는 방법을 모르기 때문에, IEx에 로드하고 인터프리트할 수 있는 .exs 파일을 사용하겠습니다.

따라서 앞으로 나올 예제 중 일부는 엘릭서 소스 코드 파일에서 가져온 것이고 나머지는 제가 IEx에 입력한 내용에서 가져온 것입니다. "IEx>" 프롬프트의 유무로 구분할 수 있습니다.

모듈

모듈은 엘릭서에서 함수를 캡슐화하고 구성합니다. 객체 지향 언어는 일반적으로 데이터와 해당 데이터가 작동하는 함수가 포함되어 있지만, 엘릭서는 함수와 데이터가 분리되어 있습니다.

관련된 함수들은 모듈에 함께 그룹화됩니다. 동일한 데이터 구조에서 작동하는 함수는 일반적으로 자체 모듈에 함께 있습니다. 예를 들어 엘릭서 표준 라이브러리의 모든 문자열 조작 함수는 String 모듈에 함께 그룹화되어 있고, 리스트와 관련된 모든 함수는 List 모듈에 있습니다.

모듈을 정의하는데 필요한 구문을 살펴봅시다. 모듈은 defmodule 키워드와 모듈 내용을 doend 사이에 넣어 정의합니다.

모듈은 IEx에서 인라인으로 정의할 수 있지만, 그렇게 하는 것은 매우 지저분하고 번거롭습니다.

iex> defmodule ExampleModule do
...> end

{:module, ExampleModule,
 <<70, 79, 82, 49, 0, 0, 3, 36, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 127,
   0, 0, 0, 12, 20, 69, 108, 105, 120, 105, 114, 46, 69, 120, 97, 109, 112, 108,
   101, 77, 111, 100, 117, 108, 101, 8, 95, ...>>, nil}

대신에 모듈은 일반적으로 소스 코드 파일에 정의됩니다.

defmodule ExampleModule do
  
end

그러면 이 "ExampleModule"이란 이름의 모듈의 본문은 do 키워드로 시작하여 end 키워드로 끝납니다.

모듈의 네임스페이스는 "[namespace2].[module]" 또는 "[namespace1].[namespace2].[module]"로 지정할 수도 있습니다. 이렇게 하면 이름이 같은 모듈이 서로 다른 네임스페이스에 있을 수 있습니다.

defmodule Cars.Driving do
  
end

혹은

defmodule Vehicles.Cars.Driving do
  
end

이것은 다음과 같습니다.

defmodule Cars do
  defmodule Driving do
  
  end
end

모듈은 단일파일에 정의해야 하며, 여러 소스 코드 파일에 나눌 수 없습니다. 그러나 단일 파일에 여러 모듈을 정의할 수는 있습니다.

흥미로운 사실: 모듈 이름은 내부적으로(Erlang VM에서) 아톰으로 변환되고 그 앞에 "Elixir"가 추가됩니다. 따라서 Stuff 모듈은 실제로 내부적으로 ":Elixir.Stuff"로 표시됩니다. (아톰에 공백이나 점 같은 특수문자가 포함될 때는 앞뒤로 따옴표를 붙여야 합니다.)

제가 본 몇 가지 예제에서 얼랭 모듈 이름도 아톰으로 되어있으며, 얼랭 VM이 모듈에 아톰이 필요하기 때문에 엘릭서가 이렇게 되어있는 것으로 생각됩니다. 모듈 이름 앞에 "Elixir"가 붙기 때문에 얼랭 VM을 모니터링하는 사람 누구나 어떤 모듈이 엘릭서 환경에서 제공되는지 확인할 수 있습니다.

기명 함수

엘릭서 함수는 일반적으로 이름이 지정되며, 이를 "기명 함수"라고 합니다. 함수는 def 키워드와 매개변수 리스트로 정의됩니다. 함수에는 함수의 내용을 포함하는 본문도 있습니다. 모듈과 마찬가지로 함수 본문은 do 키워드로 시작하고 end 키워드로 끝납니다.

defmodule ExampleModule do
  def do_something(parameter1, parameter2) do
    
  end
end

위의 예제는 ExampleModule 모듈에서 do_something 함수를 정의합니다. 이 함수는 두 개의 매개변수 parameter1parameter를 받습니다. 함수 본문에 아무것도 넣지 않았기 때문에 아무것도 반환하지 않습니다.

함수의 마지막 표현식의 값은 함수에서 반환되는 값입니다. 두 숫자를 더하는 예제 함수를 살펴보겠습니다.

def sum(num1, num2) do
    num1 + num2
end

C#과 자바스크립트와는 다르게 return 키워드가 필요하지 않습니다. 마지막 구문의 값이 자동으로 함수에서 반환됩니다.

전달된 매개변수를 반환하는 함수를 정의할 수 있습니다.

def echo(parameter) do
    parameter
end

기명 함수는 항상 모듈의 컨텍스트에서 정의해야 합니다! 기명 함수는 모듈 외부에 존재할 수 없습니다.

단일행 함수

함수가 매우 간단하고 한 줄로만 구성된 경우 더 간결한 구문을 사용할 수 있습니다. 더 간결한 구문을 사용하여 substract 함수를 작성해 보겠습니다.

def subtract(num1, num2), do: num1 - num2

쉼표는 함수 이름과 매개변수를 본문에서 구분하며, 엘릭서가 한 줄로 된 함수 본문을 예상하도록 알려줍니다.

함수 이름

엘릭서에서 기명 함수는 이름과 해당 함수의 애리티로 지정됩니다. 애리티는 함수형 프로그래밍에서 많이 사용되는 용어입니다. 간단히 말해서, 함수의 애리티는 허용되는 매개변수의 수입니다. 따라서 위에 나온 sum 함수는 2개의 파라미터를 받아들이기 때문에 애리티가 2입니다.

엘릭서에서 함수 네이밍 형식은 [이름]/[애리티]이므로, 위에서 나온 sum 함수는 엘릭서에서 sum/2로 나타냅니다.

엘릭서에서 애리티는 함수의 식별에 필수적인 부분이므로, 엘릭서에서는 do_something/1 함수를 do_something/2 와 완전히 별개의 함수로 간주합니다.

엘릭서에서는 이름이 같고 애리티가 다른 함수가 있는 것이 매우 흔한 일입니다. 엘릭서 표준 라이브러리 문서에 있는 함수는 그 함수의 식별에 필수적인 부분이기 때문에 애리티와 함께 나열되어 있습니다.

저는 모듈에 이름과 애리티가 같은 두 함수를 다음과 같이 지정하면 어떻게 될지 궁금했습니다.

defmodule ExampleModule do
  def do_something(parameter1) do
    "Carrot"
  end

  def do_something(parameter1) do
    parameter1
  end
end

에러가 발생할 것으로 예상했지만, 엘릭서는 두 번째 함수는 절대 사용되지 않는다는 경고만 표시한 채 모듈을 로드합니다. do_something/1 함수가 호출되면 정의된 함수의 첫 번째 인스턴스만 호출되고 두 번째 인스턴스는 무시됩니다. 흥미롭습니다.

익명 함수

익명 함수는 이름과 연결되지 않은 함수입니다. 여러 언어에서 "람다"라고 불리는 익명 함수는 정의하고 변수에 할당하거나 다른 함수에 전달하는 것이 일반적입니다.

자바스크립트에서는 일반적으로 다음 예제와 같은 작업을 수행합니다.

var add = function(x, y) {
	return x + y;
};

array.map(function(number) {
	return number * 2;
});

익명 함수가 생성되어 변수에 할당됩니다. 배열의 맵 함수와 같은 다른 함수에 전달할 수도 있습니다.

ES6 람다 구문에서도 마찬가지입니다.

const add = (x, y) => x + y;

array.map(number => number * 2);

C#에는 최신 C# 코드에서 자주 사용되는 람다도 있습니다.

Func<int, int, int> add = (int x, int y) => x + y;

var transformedList = list.Select(number => number * 2).ToList();

엘릭서에서 익명 함수는 fn으로 시작하고 end로 끝납니다. 함수의 매개변수와 본문은 ->로 구분됩니다.

iex> add = fn (x, y) -> x + y end
#Function<12.99386804/2 in :erl_eval.expr/5>

위의 함수는 두 개의 매개변수를 받아 두 매개변수의 합을 반환합니다.

함수 매개변수 주위의 괄호는 생략할 수도 있습니다.

iex> add = fn x, y -> x + y end
#Function<12.99386804/2 in :erl_eval.expr/5>

익명 함수는 일반적으로 특정 상황에서만 적용되고 재사용 되지 않는 수명이 짧은 함수입니다. 여러 곳에서 동일한 익명 함수를 지정하는 경우 이를 기명 함수로 전환하는 것이 좋습니다.

매개변수가 없는 익명 함수는 fn-> 사이에 아무것도 넣지 않음으로써 정의할 수 있습니다.

iex> hello = fn -> "hello" end
#Function<20.99386804/0 in :erl_eval.expr/5>

위의 함수는 단순히 "Hello" 값을 반환합니다.

익명 함수는 일반적으로 단일 표현식을 포함하지만, 여러 표현식으로 함수를 정의할 수도 있습니다. 엘릭서의 다른 함수와 마찬가지라 마지막 표현식의 값은 함수에서 반환되는 값입니다.

iex> double_and_add = fn x ->
...> d = x * 2
...> d + 1
...> end
#Function<6.99386804/1 in :erl_eval.expr/5>

엘릭서는 end 키워드를 볼 때까지 함수가 끝나지 않았다는 것을 알고 있습니다. 세미콜론을 사용하여 두 표현식을 같은 줄에 넣을 수도 있습니다.

iex> double_and_add = fn x -> d = x * 2; d + 1 end
#Function<6.99386804/1 in :erl_eval.expr/5>

두 가지 표현식을 한 줄에 넣지 않는 것이 좋습니다. 읽기가 훨씬 더 어렵습니다. 실제로 함수가 단일 표현식보다 더 복잡하다면 기명 함수로 만드는 것을 고려할 수 있습니다. 단순하고 명확할수록 좋습니다.

단축

익명 함수에는 익명 함수를 더욱 간결하게 만드는 단축 구분이 있습니다.

위에서 지정한 덧셈 함수 fn (x, y) -> x + y end&(&1 + &2)로 다시 작성할 수 있습니다. 전체는 &()로 묶어야 하며, 그 안에 포함된 표현식은 숫자로 매개변수를 나타냅니다. 따라서 &1은 첫 번째 매개변수에 해당하고 &2는 두 번째 매개변수에 해당합니다. 엘릭서는 표현식을 살펴보고 이것이 두 개의 매개변수가 있는 함수라는 것을 알아냅니다.

iex> add = &(&1 + &2)
&:erlang.+/2

저는 이 특정 익명 함수의 결과가 특히 흥미롭고 예상치 못한 결과라는 것을 알았습니다. 익명의 함수에 대해 IEx가 일반적으로 표시하는 #Function<... in :erl_eval.expr/5>을 반환하는 대신 &:erlang.+/2를 표시했습니다. 제가 정의한 함수가 기본 얼랭 환경에서 애리티가 2인 + 연산자와 정확히 동일한 함수라는 것을 인식한 것 같습니다.

그래서 기존 함수의 중복 복사본을 만드는 대신 엘릭서는add 변수를 같은 함수인 얼랭 +에 바인딩했습니다. 엘릭서는 두 함수가 동일할 경우 이를 감지하여 이미 존재하는 함수를 재사용 할 수 있습니다. 그로 인하여 메모리를 절약하고 코드를 더 효율적으로 만들 수 있습니다. 엘릭서에서 이런 기능이 가능하다는 점이 인상적이었습니다.

매개변수 1과 3을 더하는 더하기 함수를 만들 수 있는지 궁금했지만 그것은 불가능합니다. 매개변수 번호 사용을 건너뛸 수 없다는 것이 밝혀졌습니다. 이 기능을 사용하려면 표현식 어딘가에 &2를 사용해야 합니다.

iex> add = &(&1 + &3)
** (CompileError) iex:13: capture &3 cannot be defined without &2
    (elixir) src/elixir_fn.erl:126: :elixir_fn.validate/4
    (elixir) src/elixir_fn.erl:117: :elixir_fn.capture_expr/5

동일한 표현식에서 매개변수를 여러 번 재사용할 수 있습니다. 아래는 숫자의 제곱을 반환하는 함수의 예시입니다.

square = &(&1 * &1)
#Function<6.99386804/1 in :erl_eval.expr/5>

특정 상황에서 단축 함수 주변의 괄호를 다른 구분 기호로 대체할 수 있습니다.

예를 들어:

create_tuple = &({&1, &2})

처음 두 매개변수에서 튜퓰을 구성하는 익명 함수입니다. 이 경우 괄호는 생략할 수 있습니다.

create_tuple = &{&1, &2}

몇 가지 변형할 수 있는 것들을 살펴보겠습니다.

튜플:

iex> create_tuple = &({&1, &2})
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> create_tuple.(3, 4)
{3, 4}
iex> create_tuple = &{&1, &2}
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> create_tuple.(3, 4)
{3, 4}

리스트:

iex> create_list = &[&1, &2]
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> create_list.(3, 4)
[3, 4]

맵:

iex> create_map = &%{first: &1, second: &2}
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> create_map.(3, 4)
%{first: 3, second: 4}

문자열:

iex> create_string = &"The first parameter is #{&1} and the second parameter is #{&2}"
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> create_string.(3, 4)
"The first parameter is 3 and the second parameter is 4"

이 방법은 바이너리 리터럴이나 캐릭터 리스트와 같은 다른 리터럴에도 동작할 수 있습니다.

짧고 간단한 함수에 단축 구문을 사용해야 합니다. 그렇지 않으면 단축 구문은 매개변수가 이름이 아닌 번호로 참조되므로 코드를 읽기가 어려워질 수 있습니다.

기명 함수 참조

익명 함수는 다른 함수에 전달하거나 변수에 그대로 바인딩할 수 있습니다. 기명 함수는 그렇게 할 수 없습니다. 캡처 연산자 &로 참조해야 합니다.

iex> func = &ExampleModule.add/2
&ExampleModule.add/2

이것이 왜 필요한지는 잘 모르겠지만, 아마도 다른 이유로 발생할 수 있는 모호함을 해결해 줄 수 있을 것으로 생각됩니다.

함수 호출

함수를 호출할 수 없는 상태에서 함수를 정의하는 것은 쓸모가 없으므로 이제 함수를 호출하는 방법을 살펴보겠습니다.

엘릭서에서 기명 함수를 호출하는 방법은 두 가지가 있습니다. 괄호로 둘러싸인 매개변수와 함께 함수 이름을 지정하거나, 함수 이름과 매개변수를 공백으로 구분할 수 있습니다.

ExampleModule.do_something(parameter1, parameter2)

ExampleModule.do_something parameter1, parameter2

어떤 스타일을 선택할지는 여러분의 선택에 달려있습니다. 다른 엘릭서 개발자들은 서로 다른 스타일을 사용할 것입니다. 엘릭서에서 공백 스타일이 모호한 경우가 있어 괄호가 필요한 경우도 있지만 대부분의 경우 공백을 사용하면 됩니다. 저는 개인적으로 자바스크립트와 C#을 사용하던 경험 때문에 괄호로 호출하는 스타일에 좀 더 익숙합니다.

ExampleModule.do_something과 같이 인자가 없는 기명 함수만 지정하면, 엘릭서는 이것이 함수 호출이라고 가정합니다. 괄호는 선택 사항이므로, 이것은 ExampleModule.do_something()과 동일합니다.

익명 함수는 점과 괄호를 사용하여 호출해야 합니다.

iex> do_something = fn (x) -> x + 1 end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex> do_something.(3)
4

처음에 익명 함수와 기명 함수의 호출 구문 간의 차이가 정말 짜증났습니다. 그런 다음 이 구문이 익명 함수뿐만 아니라 변수에 바인딩된 기명 함수에도 실제로 적용됨을 깨달았습니다.

이 예제에서는 인자 개수가 1인 동일한 값을 반환하는 echo라는 기명 함수를 만들었습니다. 점 없이 기명 함수를 호출할 수 있습니다. 캡처 연산자를 사용하여 함수를 변수에 할당하면, 점 표기법으로 호출해야 합니다.

iex> ExampleModule.echo("Bob")
"Bob"
iex> echoFunc = &ExampleModule.echo/1
&ExampleModule.echo/1
iex> echoFunc("Bob")
** (CompileError) iex:19: undefined function echoFunc/1

iex> echoFunc.("Bob")
"Bob"

함수 변수가 함수 호출을 위해 점 표기법을 사용해야 하는 정확한 이유는 확실하지 않지만, 동일한 네임스페이스에서 기명 함수와 변수를 혼동하지 않도록 하는 실용적인 이유가 있을 것입니다. 위의 예에서 점 표기법 없이 함수 변수를 호출하려고 하면 엘릭서는 변수로 인식하지 않고, 변수와 같은 이름의 함수를 찾으려고 시도합니다. 점 표기법은 엘릭서에게 이것이 변수이고 기명 함수가 아님을 나타내야 합니다.

그래서 처음처럼 짜증이 나지는 않았지만, 문법이 일관되기를 바랍니다. 아직 보지 못한 다른 이유로 인해 언어 설계자가 이 결정을 내릴 수도 있다고 생각하기 때문에 이 초기 단계에서 결론에 도달하지 않으려고 합니다. 엘릭서에 대해 배울 것이 아직 많이 남아 있습니다.

IEx에서 모듈 로드하기

IEx에서 모듈을 어떻게 로드하는지는 언급하지 않았으므로 이 글을 마치기 전에 다뤄보겠습니다. c 명령은 모듈이 포함된 파일을 로드하고 컴파일합니다.

따라서 c "example_module.exsc("example_module.exs")를 입력하여 ExampleModule 모듈을 로드했습니다. 그런 다음 IEx는 메모리에 소스 파일을 로드하고 컴파일하여 IEx에서 사용할 수 있게 합니다. 다음 게시물에서 IEx에서 모듈을 작성하고 사용하여 지금까지 배운 내용을 실제로 적용하는 방법을 보여드리겠습니다.

이 코드 중 일부는 Learn With Me: Elixir repository 코드 예제에 있는 "lwm 11- 모듈과 함수" 폴더에서 찾을 수 있습니다.

참고로 IEx 명령어나 엘릭서 키워드 및 모듈에 대해 알고 싶다면 h 함수를 사용하면 됩니다. 저는 h c를 입력하여 c 명령어에 대해 자세히 알아봤는데, 이는 실제로 IEx에서 사용할 수 있는 엘릭서 함수입니다.

iex> h c

                        def c(files, path \\ :in_memory)

Compiles the given files.

It expects a list of files to compile and an optional path to write the
compiled code to. By default files are in-memory compiled. To write compiled
files to a current directory use empty string "" for the path. When compiling
one file, there is no need to wrap it in a list.

It returns the names of the compiled modules.

If you want to recompile an existing module, check r/1 instead.

## Examples

    iex> c(["foo.ex", "bar.ex"], "ebin")
    [Foo, Bar]

    iex> c("baz.ex")
    [Baz]

잠시만요, 더 있습니다!

아직 함수와 모듈에 대한 주제가 끝나지 않았습니다. 이야기할 내용이 너무 많아서 이 주제를 여러 개의 포스팅으로 나눠야 했습니다. 다음 포스팅에서는 좀 더 자세히 알아보고 간단한 코드 모듈을 만들고 IEx에서 해당 함수를 호출하여 지금까지 배운 내용을 연습해 보겠습니다.