Skip to content

Latest commit

 

History

History
550 lines (390 loc) · 29.8 KB

Modules and Functions: Part 2 (#11).livemd

File metadata and controls

550 lines (390 loc) · 29.8 KB

Modules and Functions: Part 2 (#11)

도입부

엘릭서 모듈과 함수에 대해 자세히 알아봅시다. 중요한 주제이므로 다뤄야 합니다.

이 주제의 1부에서는 모듈, 함수 그리고 그것을 호출하는 방법에 대해 소개했습니다. 엘릭서의 함수에 관해 이야기할 내용이 많으니 계속 이어 나가겠습니다.

디폴트 매개변수 값

엘릭서 함수 매개변수는 각 매개변수 뒤에 \\를 사용하여 기본값을 지정할 수 있습니다.

def do_something(item, count \\ 1)

따라서 위 함수에서 함수를 호출할 때 count 매개변수를 지정하지 않으면 count 매개변수는 기본값 1이 됩니다. 즉 do_something("Bob")을 호출하면 count는 기본값 1이 되고 do_something("Bob", 4)를 호출하면 count는 4의 값을 가지게 됩니다.

엘릭서는 기본값을 가질 때 실제로 여러 함수를 정의합니다. 이 경우 do_something/1do_something/2를 생성합니다. 애리티가 1인 함수는 기본값을 전달하여 애리티가 2인 함수를 호출합니다.

매개변수 기본값에 대해 배울 때 찾은 정보는 이것뿐이었기 때문에 기본값이 자바스크립트나 C#과 어떻게 다른지 알아보기 시작했습니다.

자바스크립트에서 모든 매개변수는 선택 사항입니다. 매개변수에 기본값을 지정하는 경우(ES6 이상)와 호출자가 기본값을 지정하지 않은 경우 해당 매개변수에 기본값이 할당됩니다. 그렇지 않은 경우 해당 매개변수는 undefined를 가져옵니다.

다음 자바스크립트 함수를 살펴보겠습니다.

function do_something(a, b = 1, c, d) {
  return [a, b, c, d];
}

do_something("Bob")을 호출하면 ["Bob", 1, undefined, undefined]이 반환됩니다. 기본값도 없고 호출자가 값을 제공하지 않은 매개변수는 기본값이 undefined로 설정됩니다.

반면에 C#에서는 기본값이 있는 모든 매개변수는 맨 끝에 있어야 합니다. C#에서 기본값이 있는 옵셔널한 매개변수는 필수 매개변수(기본값이 없는 매개변수)보다 앞에 올 수 없습니다.

따라서 다음 예제에서는 컴파일 오류가 발생합니다.

public List<int> DoSomething(int a, int b = 1, int c, int d)
{
	return new List<int> {a, b, c, d};
}

기본값을 끝으로 이동시키면 코드가 컴파일될 수 있습니다.

public List<int> DoSomething(int a, int b, int c, int d = 1)
{
	return new List<int> {a, b, c, d};
}

30초 가량 검색해도 이 상황에서 엘릭서가 어떻게 동작하는지 알 수 없었기 때문에 이를 확인하기 위해 직접 사용해 보기로 했습니다. 엘릭서는 제가 예상한 대로 동작하지 않았습니다.

엘릭서에서 다음 함수를 만들었습니다.

def create_list(a, b \\ 1, c, d) do
	[a, b, c, d]
end

그런 다음 세 개의 매개변수를 사용하여 호출했습니다. 처음 세 개(a, b, c)를 채운 다음 d에 대한 값이 없어서 오류가 발생할 것으로 예상했지만, 다음과 같이 나왔습니다.

iex> DefaultValues.create_list("Bob", 4, 3)
{"Bob", 1, 4, 3}
iex> DefaultValues.create_list("Bob", :ok, 4, 3)
{"Bob", :ok, 4, 3}

엘릭서가 do_something/3 함수와 do_something/4 함수를 생성한 것으로 나타났습니다. do_something/3 함수는 필요한 매개변수만 가져와서 두 번째 매개변수를 기본값으로 하고 do_something/4 함수를 호출했습니다. 매우 흥미로웠습니다. 매개변수의 아무 곳에나 기본값을 넣은 다음 매개변수의 위치와 관계없이 필요한 매개변수만 전달할 수 있는 효과가 있습니다. 필수 매개변수가 먼저 채워집니다.

엘릭서가 기본값을 가지고 호출하는 대체 함수를 생성한다는 사실을 알고 나니 이해가 됩니다. 선택적 매개변수가 어디에 있는지는 상관없습니다.

비공개 함수

접근 제한 함수는 모듈 내에서만 볼 수 있는 모듈의 함수입니다. 모듈 외부에서 참조하거나 호출할 수 없습니다. 공개 함수는 def 키워드를 사용하여 정의하는 반면, 비공개 함수는 defp 키워드를 사용하여 정의합니다.

defmodule ExampleModule do
	defp private_function(value) do
		
	end
end

함수와 모듈 명명 규칙

함수와 모듈에는 모든 엘릭서 개발자가 사용하는 명명 규칙이 있습니다. 이러한 규칙을 벗어난다고 해서 엘릭서가 망가지진 않지만 다른 엘릭서 개발자가 코드를 읽고 이해하기는 더 어려워질 수 있습니다.

변수와 마찬가지로 함수도 스네이크 케이스입니다. do_something_unexpected와 같이 단어 사이에 밑줄이 있는 소문자를 사용하여 지정합니다.

변수와 마찬가지로 밑줄(_)로 시작하는 함수 이름은 함수를 사용해서는 안 된다는 것을 나타냅니다. 함수를 비공개 함수로 정의하지 않고 이런 식으로 정의하는 이유는 잘 모르겠지만 제가 알지 못하는 사용 사례가 있을 것입니다. 아마도 특정 모듈은 사용할 수 있지만 (C++의 프랜드 개념과 비슷하게) 다른 모듈은 사용할 수 없도록 하기 위해 이런 식으로 정의한 것일까요? 이에 대한 예시와 그 이유를 알고 싶습니다.

일반적으로 불리언 값을 반환하는 함수 이름 끝에는 "?" 문자를 추가합니다. 예를 들어 is_available?가 있습니다. 가드 절에서 사용하는 함수는 예외입니다. 불리언 값을 반환하지만 함수 이름 끝에 "?"가 없습니다. 현재로써는 가드 절이 무엇인지 모르지만 앞으로 배워야할 부분입니다.

일반적으로 실패 시 예외를 발생시키는 함수 끝에는 "!" 문자가 추가됩니다. 엘릭서 I/O 라이브러리에서는 같은 함수의 실패 시 예외를 발생시키는 것과 발생시키지 않는 버전을 가지는 많은 함수가 있으며, 두 함수의 차이점은 함수 이름 끝에 "!" 문자가 있다는 점입니다.

예를들어 File.write/3File.write!/3가 있습니다.

예외를 발생시키지 않는 함수는 일반적으로 결과(:ok 또는 :error)와 발생한 모든 데이터가 포함된 튜플을 반환합니다.

엘릭서 함수 이름이 "size"인 경우 해당 연산이 상수 시간(O(1))으로 실행됩니다. 이는 일반적으로 크기가 데이터 구조에 함께 저장되기 때문입니다.

엘릭서 함수 이름이 "length"인 경우 해당 연산이 선형 시간(O(n))으로 실행됩니다. 이는 일반적으로 데이터 항목의 개수를 파악하기 위해 알고리즘이 데이터 항목을 반복하기 때문입니다.

함수 매개변수가 사용되지 않을 경우 이름 앞에 밑줄이 표시됩니다. 함수의 여러 버전에서 매개변수가 한 버전에서는 사용되지만 다른 버전에서는 사용되지 않는 경우가 종종 있습니다. 밑줄은 코드를 읽는 사람에게 정보를 제공할 뿐만 아니라 함수의 파라미터를 사용하지 않았다고 경고를 내보내 컴파일을 억제합니다.

컴파일러가 사용되지 않은 매개변수를 감지할 때 발생하는 경고의 예입니다.

warning: variable "param2" is unused
iex:4

사용하지 않는 매개변수 앞에 밑줄을 넣으면 이 경고를 피할 수 있습니다.

def do_something(param1, _param2) do
	param1
end

모듈의 이름은 첫 글자가 대문자이고 모듈의 각 단어가 대문자로 시작하는 카멜 케이스를 사용하여 지정합니다.

defmodule RoadSegment do

end

다른 모듈의 함수 사용

다른 모듈에서 함수를 호출하려면 "[모듈 이름].[함수 이름]"을 호출하면 됩니다.

다음은 String 모듈에서 length 함수를 호출하는 예제입니다. String 모듈은 엘릭서 표준 라이브러리의 일부이며, 모든 표준 라이브러리 모듈은 어디서나 사용할 수 있습니다.

iex> String.length("This is a string")
16

현재로서는 엘릭서가 참조한 모듈을 어떻게 찾는지 아직 명확하지 않습니다. 일종의 검색 경로가 있는지, 아니면 엘릭서 컴파일러에서 컴파일된 모든 모듈을 사용할 수 있을지 잘 모르겠습니다. 앞으로 더 명확해질 것으로 예상합니다.

전체 모듈 가져오기

모듈의 함수를 현재 네임스페이스로 가져오려면 import 키워드를 사용하면 됩니다. 다음 예제는 String 모듈의 모든 함수를 현재 네임스페이스로 가져오는 것입니다.

iex> import String
String
iex> String.upcase("This is a string")
"THIS IS A STRING"
iex> upcase("This is a string")
"THIS IS A STRING"

함수를 가져오면 더 이상 모듈 이름을 지정할 필요가 없습니다. 함수 이름을 호출하기만 하면 됩니다.

모듈 이름을 지정하거나 아무것도 가져오지 않고도 엘릭서에서 사용할 수 있는 함수는 Kernel 모듈에 있습니다. is_atom이나 div와 같은 함수가 이에 해당합니다. 심지어 +,-,&& 또는 ===와 같은 연산자도 실제로는 Kernel 모듈에 있는 함수입니다. 엘릭서 환경은 Kernel의 모든 함수를 동으로 가져오기 때문에 Kernel.+ 또는 Kernel.is_atom을 지정하지 않고도 이러한 함수를 사용할 수 있습니다.

현재 네임스페이스로 가져온 모듈의 함수 이름이 다른 가져온 모듈의 함수와 충돌할 수 있으므로 주의해야 합니다.

예를 들어, Kernel 모듈에는 리스트의 길이를 결정하는 length/1 함수가 있습니다. 이 함수가 리스트 모듈에 없는 이유는 저에게 미스터리이지만, 그렇게 되어있습니다(문서에 따르면 가드 절에 사용할 수 있다고 나와 있는데, 그 이유가 무엇인지 궁금합니다). String 모듈에도 length/1 함수가 있습니다. String의 모든 함수를 가져와도 어떤 종류의 오류나 경고도 발생하지 않았지만, 현재 네임스페이스에 두 개의 length/1 함수가 있다는 것을 알게 되었습니다.

문자열에서 length/1을 호출하면 어떤 일이 발생하는지 살펴봅시다.

iex> import String
String
iex> length("This is a string")
** (CompileError) iex:7: function length/1 imported from both String and Kernel, call is ambiguous
    (elixir) src/elixir_dispatch.erl:111: :elixir_dispatch.expand_import/6
    (elixir) src/elixir_dispatch.erl:81: :elixir_dispatch.dispatch_import/5
iex> String.length("This is a string")
16
iex> Kernel.length([1, 2, 3])
3

흥미롭습니다. 엘릭서는 String.length/1Kernel.length/1을 모두 현재 네임스페이스로 가져왔기 때문에 호출하는 것이 모호하다고 불평합니다. 모듈 이름을 지정하여 모호함을 제거해야 합니다.

엘릭서는 다른 모듈을 로드하고 사용하기 위해 import를 사용해야 하는 파이썬과 다릅니다. 다른 네임스페이스의 코드를 현재 네임스페이스로 가져오는 C#의 using문과 비슷합니다. C#과 마찬가지로 엘릭서 코드도 import문 없이도 다른 코드를 사용할 수 있지만, 가져오게 되면 개발자가 전체 네임스페이스를 입력하지 않고도 해당 코드를 참조할 수 있습니다.

밑줄로 시작하는 모듈 내 함수는 관례상 사용하면 안 되는 함수이므로 자동으로 가져오지 않습니다. 만약 사용해야 하는 경우 이름별로 구체적으로 가져와야 합니다.

모듈에서 특정 함수 가져오기

엘릭서 가이드라인에서는 함수 이름 충돌의 가능성이 있고 코드를 읽는 사람이 특정 함수가 실제로 어느 모듈에서 왔는지 알 수 없는 불명확한 코드가 생성될 수 있으므로 모듈의 모든 함수를 가져오는 것을 권장하지 않습니다. 대신 가이드라인에서는 모듈 이름을 지정하거나 모듈에서 특정 함수를 이름으로 가져올 것을 권장하며, 이를 통해 어떤 함수를 가져오는지, 어떤 모듈에서 가져오는지 정확히 알 수 있습니다.

only: 구문을 사용하여 모듈에서 특정 함수를 가져올 수 있습니다. String 모듈에서 몇 가지 함수를 가져오는 예시를 살펴보겠습니다.

iex> import String, only: [upcase: 1, trim: 1]
String
iex> upcase("This is a string")
"THIS IS A STRING"
iex> trim("  This is a string  ")
"This is a string"
iex> split("This is a string")
** (CompileError) iex:14: undefined function split/1

iex> String.split("This is a string")
["This", "is", "a", "string"]
iex> length([1, 2, 3])
3

이 예제에서는 String 모듈에서 upcase/1trim/1만 가져왔습니다. import 구문 upcase: 1upcase/1에 해당합니다. 이제 가져온 두 함수를 모두 사용할 수 있었지만 가져오지 않은 String 모듈의 다른 함수인 String.split/1은 현재 네임스페이스에서 여전히 사용할 수 없습니다.

String에서 특정 함수를 가져오면서 String.length/1을 가져오지 않았기 때문에 length/1 함수 이름 충돌을 피할 수 있습니다. 이런 식으로 함수를 가져오면 upcase/1trim/1 함수의 출처도 명확해집니다.

또한 only:와 같은 맥락으로 사용되지만 반대 효과가 있는 except:도 있습니다. 이 함수는 나열된 함수를 제외한 모든 함수를 가져옵니다.

다른 가져오기 방법

C#의 "using" 문과 마찬가지로 모듈을 현재 네임스페이스로 가져오는 대신 네임스페이스 별칭을 만들 수 있습니다. 이 작업은 alias 키워드로 수행됩니다.

iex> alias String, as: Bob
String
iex> Bob
String
iex> Bob.length("This is a string")
16
iex> Bob.upcase("This is a string")
"THIS IS A STRING"
iex> String.upcase("This is a string")
"THIS IS A STRING"

이 예제에서는 String 모듈을 Bob이라는 별칭으로 만들었습니다.

alias를 사용하여 긴 네임스페이스를 간소화할 수 있습니다. NameSpace1.Namespace2.Category.Subdivision.ExampleModule과 같은 네임스페이스는 ExampleModule로 별칭을 지정할 수 있습니다. 이렇게 하면 해당 모듈의 함수를 현재 네임스페이스로 가져오지 않고도 해동 모듈의 함수를 훨씬 쉽게 참조할 수 있습니다.

as: 없이 alias를 사용하는 경우 별칭은 모듈 이름의 마지막 부분으로 설정됩니다. 따라서 as:가 없는 alias NameSpace1.Namespace2.Category.Subdivision.ExampleModule는 마지막 점 다음에 오는 모듈 이름인 ExampleModule가 별칭으로 설정됩니다.

alias와 import 모두 범위가 지정되므로 원하는 경우 파일 수준, 모듈 내부 또는 개별 함수 내부에 alias와 import를 넣을 수 있습니다. 함수 내에 포함된 alias나 import는 해당 함수 범위 내에서만 적용되며 같은 모듈의 다른 함수에서는 사용할 수 없습니다.

defmodule ExampleModule do
	def atom_to_string_one(atom) do
		Atom.to_string(atom)
	end
	
	def atom_to_string_two(atom) do
		import Atom, only: [to_string: 1]
		to_string(atom)
	end
	
	def atom_to_string_three(atom) do
		alias Atom, as: Waffle
		Waffle.to_string(atom)
	end	
end

use 매크로는 모듈의 함수를 현재 네임스페이스로 가져올 뿐만 아니라 해당 모듈에서 모듈을 사용하기 전에 수행해야 하는 몇 가지 설정 작업을 수행하는 코드를 실행합니다. 현재로서는 매크로에 무엇이 포함되는지, 어떻게 사용하는지, 엘릭서의 컨텍스트에서 매크로가 무엇인지 정확히 알지 못하지만, 매크로가 사용할 수 있는 도구라는 것 정도는 알 수 있습니다. 좀 더 심화 기능을 배우게 되면 use 매크로와 매크로 전반에 대해 더 많이 배울 수 있을 것으로 기대합니다.

모듈 어트리뷰트

모듈 어트리뷰트는 상수로 사용할 수 있는 일종의 메타데이터 또는 데이터를 포함하는 주석을 나타냅니다. 모듈 어트리뷰트는 "@" 문자와 소문자로 시작합니다.

defmodule ExampleModule do
	@test_file_name "test_file.txt"
	
	def test_file() do
		@test_file_name
	end
end

이 예제에서는 @test_file_name이라는 어트리뷰트를 정의하고 여기에 "test_file.txt"라는 값을 할당합니다. 이 어트리뷰트는 모듈 내 어디에서나 사용할 수 있습니다. 여기서는 이 속성을 상수처럼 사용합니다. 이 속성은 모듈 외부에서는 사용할 수 없는 것으로 보입니다.

iex> ExampleModule.test_file
"test_file.txt"
iex> ExampleModule.@test_file_name
** (CompileError) iex:23: undefined function test_file_name/0

모듈 어트리뷰트는 재정의할 수 있으며 컴파일러는 코드를 읽을 때 현재 할당된 값을 사용합니다. 이는 또한 동일한 어트리뷰트를 문제없이 메타데이터 용도로 재사용할 수 있음을 의미합니다.

defmodule ExampleModule do
	@test_file_name "test_file.txt"
	
	def test_file() do
		@test_file_name
	end
	
	@test_file_name "another_test_file.txt"
	
	def second_test_file(), do: @test_file_name
end
iex> ExampleModule.test_file
"test_file.txt"
iex> ExampleModule.second_test_file
"another_test_file.txt"

이 예제에서는 괄호를 사용하지 않고 함수를 호출하였습니다. 상수만 반환하는 함수는 (실제로 함수임에도 불구하고) 모듈 상수처럼 느껴지므로 괄호를 생략했습니다. 엘릭서는 그렇게 해도 괜찮습니다.

모듈 어트리뷰트는 함수 내부가 아닌 모듈 수준에서만 정의할 수 있습니다.

아직 어트리뷰트가 어떻게 사용되는지 많이 배운 것 같지 않습니다. 모듈과 함수에 어노테이션으로 자주 사용된다는 것은 알고 있지만, 실제 엘릭서 코드에서 어떻게 사용되는지 아직 파악하지 못한 것 같습니다. 나중에 실제 엘릭서 코드를 살펴보면 일반적으로 어떻게 사용되는지 더 자세히 알 수 있을 것으로 기대합니다.

모듈 어트리뷰트는 컴파일 타임에만 사용할 수 있으며 런타임에 메타데이터를 쿼리하여 검색할 수 없다는 것을 알고 있습니다. 따라서 어트리뷰트 값을 사용하는 모든 코드는 컴파일 중에 해당 값이 삽입됩니다.

엘릭서 연산자 - 실제 사례

이제 실용적인 일을 할 수 있을 만큼 충분히 배웠다고 생각합니다. 저는 배운 것을 가지고 무언가를 해볼 때 가장 잘 배우기 때문에 지금은 간단한 것을 만들어 보려고 합니다. 여러분도 시간을 내서 비슷한 일을 해보시길 권합니다. 지금까지 저를 따라오면서 배운 것을 가지고 코딩을 해보세요. 호기심을 갖고 엘릭서를 가지고 놀아보세요. 이 글을 읽고 아무것도 하지 않는 것보다 훨씬 더 많은 것을 배울 수 있을 것입니다.

ElixirOperator 프로젝트는 깃허브의 Learn With Me: Elixir repository 의 project 폴더 아래에 있는 자체 폴더에서 찾을 수 있습니다. 프로젝트를 다운로드하여 따라 할 수 있습니다.

먼저 math_operations.exs에 있는 MathOperations 모듈을 살펴봅시다.

defmodule MathOperations do
	@pi 3.141592653589793
	
	def add(x, y), do: x + y
	def subtract(x, y), do: x - y
	def multiply(x, y), do: x * y
	def divide(x, y), do: x / y
	def mod(x, y), do: rem(x, y)
	def negate(x), do: -x
	def square(x), do: x * x
	def pi(), do: @pi
end

이 모듈에는 다양한 수학 함수가 포함되어 있습니다. add, subtract, multiply, divide 함수는 매우 직관적입니다. mod 함수는 모듈러 연산을 수행하며, C#과 자바스크립트에서는 % 연산자로 표시됩니다. negate 함수는 숫자를 받아 부호를 반전시키고, square 함수는 숫자를 제곱하며, pi 함수는 단순히 모듈 어트리뷰트에 저장된 파이 값을 반환합니다. 전체적으로 매우 간단한 모듈이며 모든 함수를 단일 라인 함수로 작성할 수 있습니다.

이제 c 함수를 사용하여 이 모듈을 IEx에 로드해보겠습니다. 경로는 현재 디렉토리를 기준으로 합니다. 쉽게 하기 위해 ElixirOperator 디렉토리로 이동하여 IEx를 시작합니다.

iex> c "math_operations.exs"
[MathOperations]

이제 모듈이 IEx에 로드되었고 함수를 호출할 수 있습니다.

iex> MathOperations.add(10, 23)
33
iex> MathOperations.subtract(5, 15)
-10
iex> MathOperations.multiply(5, 3)
15
iex> MathOperations.divide(10, 3)
3.3333333333333335
iex> MathOperations.mod(8, 2)
0
iex> MathOperations.mod(8, 3)
2
iex> MathOperations.negate(9)
-9
iex> MathOperations.negate(-17)
17
iex> MathOperations.square(5)
25
iex> MathOperations.pi
3.141592653589793
iex> MathOperations.pi()
3.141592653589793

훌륭합니다! 유용한 수학 연산이 포함된 모듈을 만들었습니다. 그다지 인상적이지는 않지만 엘릭서에 익숙해지는 데 좋은 연습이 되었습니다.

또한 고차 함수를 만드는 연습을 해보고 싶어서 연산자 모듈을 만들게 되었습니다. 연산자 모듈의 함수는 하나 이상의 함수와 일부 데이터를 수신하고 해당 데이터를 가지고 해당 함수를 호출합니다.

한 번 살펴보겠습니다.

defmodule Operator do
	#Takes some data and a function with one parameter and applies a function to that data
	def apply(operation, data) do
		operation.(data)
	end

	#Takes some data and a function with two parameters and applies a function to that data
	def apply(operation, data1, data2) do
		operation.(data1, data2)
	end
	
	#Takes some data and two functions, the first function with one parameter and the second function with 
	#one parameter and applies the first function to that data, with the second function fed the result
	#of the first function. The result of that will be returned.
	def apply_two(operation1, operation2, data) do
		result = operation1.(data)
		operation2.(result)
	end
	
	#Takes some data and two functions, the first function with two parameters and the second function with 
	#one parameter and applies the first function to that data, with the second function fed the result
	#of the first function. The result of that will be returned.
	def apply_two(operation1, operation2, data1, data2) do
		result = operation1.(data1, data2)
		operation2.(result)
	end
end

물론 함수를 직접 호출해도 동일한 작업을 수행할 수 있지만, 이렇게 하면 고차 함수를 구현하는 연습을 할 수 있습니다.

apply 함수는 단일 함수를 호출하여 어떤 데이터를 전달하고 결과를 반환합니다. apply_two 함수는 첫 번째 함수에 데이터를 전달하고, 첫 번째 함수의 결과를 가져와 두 번째 함수에 전달한 다음 두 번째 함수의 결과를 반환합니다. 원시적이고 직관적인 함수 파이프라인과 같습니다.

초기 데이터 --> 함수1 --> 함수2 --> 최종 결과

어떻게 Operator 모듈에서 점 표기법을 사용하여 전달된 함수를 호출하는지 살펴보세요. 이는 해당 함수가 매개변수에 바인딩되어 있고 익명함수이기 때문입니다. 매개변수가 기명 함수에 바인딩 될 수는 있지만 그 자체가 기명 함수는 아닐 수 있습니다.

모듈을 IEx에 로드하고 MathOperations 모듈의 함수와 함께 사용해 보겠습니다. 이것은 더 간단한 함수를 결합하여 더 복잡한 작업을 수행할 수 있는 함수 구성의 예입니다.

iex> c "operator.exs"
[Operator]
iex> Operator.apply(&MathOperations.add/2, 4, 9)
13
iex> Operator.apply(&MathOperations.subtract/2, 4, 9)
-5
iex> Operator.apply(&MathOperations.multiply/2, 4, 9)
36
iex> Operator.apply(&MathOperations.divide/2, 4, 9)
0.4444444444444444
iex> Operator.apply(&MathOperations.mod/2, 9, 4)
1
iex> Operator.apply(&MathOperations.negate/1, 9)
-9
iex> Operator.apply(&MathOperations.square/1, 9)
81
iex> Operator.apply(&MathOperations.square/1, MathOperations.pi)
9.869604401089358

apply 메서드에 함수를 전달하기 위해 캡처 연산자와 함수의 이름/애리티를 사용해야 하는 점에 주목하세요. 마지막에 square/1 함수를 전달하고 pi/0 함수를 호출하여 제곱할 값을 찾았습니다. 그 결과 PI가 제곱 되었습니다. MathOperations.pi/0의 반환하는 값이 아닌 함수를 전달하려면 캡처 연산자를 사용해야 했을 것입니다. 이것이 함수를 전달하는 것과 함수를 호출하고 그 결과를 전달하는 것의 문법 차입니다.

여기에서도 두 버전의 apply 함수를 호출하고 있습니다. 매개변수가 3개인 것은 Operator.apply/3과 일치하고, 매개변수가 2개인 것은 Operator.apply/2와 일치합니다.

이제 apply_two 함수를 사용해 보겠습니다.

iex> Operator.apply_two(&MathOperations.negate/1, &MathOperations.square/1, 8)
64
iex> Operator.apply_two(&MathOperations.square/1, &MathOperations.negate/1, 8)
-64
iex> Operator.apply_two(&MathOperations.add/2, &MathOperations.square/1, 5, 2)
49
iex> Operator.apply_two(&MathOperations.multiply/2, &MathOperations.square/1, 5, 2)
100
  • 첫 번째는 숫자 8을 부호를 반대로 바꿔 -8이 된 다음 제곱하여 64가 됩니다.
  • 두 번째는 연산 순서를 반대로 하여 8을 제곱하여 64가 된 다음 음수화 하여 -64가 되도록 했습니다.
  • 세 번째는 5와 2를 더하여 7을 얻은 다음 제곱합니다. 결과는 49입니다.
  • 네 번째는 5와 2를 곱하여 10을 구한 다음 제곱합니다. 결과는 100입니다.

이 작은 프로젝트를 통해 많은 것을 배웠습니다. 다양한 컴파일 오류와 이러한 함수를 호출하려고 시도하는 동안 IEx에 나온 오류는 여기서 보여주지 않았습니다. 실제로 많은 오류가 있었지만, 많은 오류로 코드 예제를 복잡하게 만드는 것은 별로 도움이 되지 않을 것 같아서 편집했습니다.

마지막 예제의 편집되지 않은 버전은 다음과 같습니다.

iex> Operation.apply(fn (x, y) -> x - (y *2) end, 10, 3)
** (UndefinedFunctionError) function Operation.apply/3 is undefined (module Operation is not available)
    Operation.apply(#Function<12.99386804/2 in :erl_eval.expr/5>, 10, 3)
iex> Operator.apply(fn (x, y) -> x - (y *2) end, 10, 3)
4
iex> Operator.apply(fn x -> x * (x + 1), 10, 3)
** (SyntaxError) iex:69: unexpected token: ). The "fn" at line 69 is missing terminator "end"

iex> Operator.apply(fn x -> x * (x + 1) end, 10, 3)
** (BadArityError) #Function<6.99386804/1 in :erl_eval.expr/5> with arity 1 called with 2 arguments (10, 3)
    projects/ElixirOperator/operator.exs:9: Operator.apply/3
iex> Operator.apply(fn x -> x * (x + 1) end, 10)
110
iex> Operator.apply(&(&1 + (&2 * &1)), 10)
** (BadArityError) #Function<12.99386804/2 in :erl_eval.expr/5> with arity 2 called with 1 argument (10)
    projects/ElixirOperator/operator.exs:4: Operator.apply/2
iex> Operator.apply(&(&1 + (&2 * &1)), 10, 3)
40
iex> Operator.apply(&(&1 + (&2 * &1)), &MathOperations.negate/1, 10, 3)
** (UndefinedFunctionError) function Operator.apply/4 is undefined or private. Did you mean one of:

      * apply/2
      * apply/3

    Operator.apply(#Function<12.99386804/2 in :erl_eval.expr/5>, &MathOperations.negate/1, 10, 3)
iex> Operator.apply_two(&(&1 + (&2 * &1)), &MathOperations.negate/1, 10, 3)
-40
iex>

첫 번째 오류는 Operator 모듈의 이름을 잘못 입력한 것이었습니다. 두 번째 오류는 end로 익명 함수를 종료하는 것을 잊어버렸기 때문입니다. 세 번째 오류는 하나의 매개변수만 허용하는 함수에 두 개의 매개변수를 적용한 것이었습니다. 네 번째 오류는 두 개의 매개변수를 허용하는 함수에 하나의 매개변수를 적용하려고 시도한 것입니다. 다섯 번째 오류는 apply_two 함수가 아닌 apply 함수에 두 개의 함수를 전달한 것이었습니다.

배우는 동안 실수를 많이 하는 것은 흔한 일이며, 실패도 모두 학습의 일부라는 것을 알아주셨으면 합니다! 실수하고 오류를 일으키고 무엇이 잘못되었는지 알아내는 과정은 엘릭서를 배우는 데 있어 필수적인 과정이었습니다. 또한 엘릭서 오류 메시지를 해석하는 연습을 할 수 있어 나중에 무엇이 잘못되었는지 훨씬 쉽게 파악할 수 있습니다. 실수를 통해 무언가를 배울 수 있다면 시간 낭비가 아닙니다.

그냥 읽기만 한 것보다 직접 해보면서 훨씬 더 많이 배웠습니다. 이쯤에서 잠시 이 글을 읽으면서 지금까지 배운 내용을 활용해 엘릭서를 사용해 보시기 바랍니다. 꽤 효과적인 학습 방법입니다.