익명 함수
익명 함수
익명 함수(anonymous function)는 실행 가능한 코드를 마치 정수나 문자열처럼 변수에 저장하고 여기저기로 전달할 수 있게 해 줘요. 이번 장에서 자세히 배워 볼게요.
본문
함수와 문서 식별하기
익명 함수를 다루기 전에, Elixir가 이름 있는 함수(모듈 안에 정의된 함수)를 어떻게 식별하는지부터 이야기해 볼게요.
Elixir의 함수는 **이름과 인자수(arity)**로 식별돼요. 인자수는 함수가 받는 인자의 개수를 말해요. 이제부터 문서 곳곳에서 함수 이름과 인자수를 함께 써서 함수를 설명할 거예요. trunc/1은 이름이 trunc이고 인자를 1개 받는 함수를 가리키고, trunc/2는 같은 이름이지만 인자수가 2인, 다른(존재하지 않는) 함수를 가리켜요.
이 문법으로 문서에 접근할 수도 있어요. Elixir 셸은 h 함수를 정의하는데, 이것으로 어떤 함수의 문서든 볼 수 있어요. 예를 들어 h trunc/1을 입력하면 trunc/1 함수의 문서가 출력돼요.
iex> h trunc/1
def trunc(number)
Returns the integer part of number.
h trunc/1이 동작하는 이유는 trunc/1이 Kernel 모듈에 정의돼 있기 때문이에요. Kernel 모듈의 모든 함수는 자동으로 우리 네임스페이스로 임포트되죠. 보통은 문서를 찾을 때 모듈 이름도 함께 포함해요.
iex> h Kernel.trunc/1
def trunc(number)
Returns the integer part of number.
모듈+함수 식별자를 사용해 연산자까지 포함해 무엇이든 문서를 찾아볼 수 있어요(h Kernel.+/2도 시도해 보세요). 인자 없이 h를 호출하면 h와 다른 기능들이 정의된 IEx.Helpers의 문서가 표시돼요.
익명 함수 정의하기
Elixir의 익명 함수는 fn과 end 키워드로 감싸요.
iex> add = fn a, b -> a + b end
#Function<12.71889879/2 in :erl_eval.expr/5>
위 예시에서 인자 두 개 a, b를 받아 a + b의 결과를 돌려주는 익명 함수를 정의했어요. 인자는 항상 -> 왼쪽에, 실행할 코드는 오른쪽에 와요. 익명 함수는 add 변수에 저장돼요. 반환 값이 #Function<...>로 표현되는 걸 볼 수 있는데, 그 표현은 불투명하지만 :erl_eval.expr 부분은 이 함수가 셸(평가 중)에서 정의됐다는 걸 알려 줘요.
익명 함수는 변수와 여는 괄호 사이에 점(.)을 찍고 인자를 넘겨 호출할 수 있어요.
iex> add.(1, 2)
3
점은 변수 add에 저장된 익명 함수를 호출하는 것인지, add/2라는 이름의 함수를 호출하는 것인지를 명확히 구분해 줘요. 예를 들어 is_atom 변수에 익명 함수가 저장돼 있을 때 is_atom.(:foo)와 is_atom(:foo) 사이에는 모호함이 없어요. 만약 둘 다 is_atom(:foo) 문법을 쓴다면, 그 동작을 알려면 지금까지의 코드 전체를 훑어 is_atom 변수가 정의됐는지 찾아봐야 해요. 이런 스캔은 개발자가 코드를 읽고 쓸 때 머릿속에 추가 문맥을 유지해야 하므로 유지보수성을 해쳐요.
Elixir의 익명 함수도 받는 인자 수로 식별돼요. is_function/1로 값이 함수인지 확인하고, is_function/2로 인자수도 확인할 수 있어요.
iex> is_function(add)
true
# add가 정확히 2개 인자를 받는 함수인지 확인
iex> is_function(add, 2)
true
# add가 정확히 1개 인자를 받는 함수인지 확인
iex> is_function(add, 1)
false
클로저
익명 함수는 함수가 정의될 때 스코프 안에 있는 변수에도 접근할 수 있어요. 이걸 보통 클로저(closure)라고 부르는데, 자신의 스코프를 닫아 안는다(close over)는 의미예요. 앞서 정의한 add 익명 함수를 사용하는 새 익명 함수를 만들어 볼게요.
iex> double = fn a -> add.(a, a) end
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> double.(2)
4
함수 안에서 할당한 변수는 바깥 환경에 영향을 주지 않아요.
iex> x = 42
42
iex> (fn -> x = 0 end).()
0
iex> x
42
절과 가드
case/2와 비슷하게, 익명 함수의 인자에 패턴 매칭을 할 수 있고 여러 절(clause)과 가드(guard)도 정의할 수 있어요.
iex> f = fn
...> x, y when x > 0 -> x + y
...> x, y -> x * y
...> end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> f.(1, 3)
4
iex> f.(-1, 3)
-3
각 익명 함수 절의 인자 수는 같아야 해요. 다르면 오류가 나요.
iex> f2 = fn
...> x, y when x > 0 -> x + y
...> x, y, z -> x * y + z
...> end
** (CompileError) iex:1: cannot mix clauses with different arities in anonymous functions
캡처 연산자
이 가이드 내내 함수를 가리킬 때 name/arity 표기법을 써 왔어요. 그런데 이 표기법은 실제로 기존 함수를 주고받을 수 있는 데이터 타입으로 캡처하는 데 쓸 수 있어요. 익명 함수처럼 동작하죠.
iex> fun = &is_atom/1
&:erlang.is_atom/1
iex> is_function(fun)
true
iex> fun.(:hello)
true
iex> fun.(123)
false
보시다시피 함수를 캡처하면 인자로 넘기거나 익명 함수 표기법으로 호출할 수 있어요. 위 반환 값은 모듈에 정의된 함수도 캡처할 수 있음을 암시해요.
iex> fun = &String.length/1
&String.length/1
iex> fun.("hello")
5
Elixir에서 연산자는 함수이므로 연산자도 캡처할 수 있어요.
iex> add = &+/2
&:erlang.+/2
iex> add.(1, 2)
3
캡처 문법은 기존 함수를 감싸는 함수를 만드는 지름길로도 쓸 수 있어요. 예를 들어 주어진 함수가 인자수가 2인지 확인하는 익명 함수를 만들고 싶다고 해 볼게요. 이렇게 쓸 수 있죠.
iex> is_arity_2 = fn fun -> is_function(fun, 2) end
#Function<8.71889879/1 in :erl_eval.expr/5>
iex> is_arity_2.(add)
true
하지만 캡처 문법을 쓰면 이렇게 돼요.
iex> is_arity_2 = &is_function(&1, 2)
#Function<8.71889879/1 in :erl_eval.expr/5>
iex> is_arity_2.(add)
true
&1은 함수에 전달되는 첫 번째 인자를 나타내요. 따라서 위에서 정의한 두 is_arity_2 익명 함수는 동등해요.
다시 말하지만 연산자는 함수 호출이므로 캡처 문법 단축형은 연산자나 심지어 문자열 보간에도 동작해요.
iex> fun = &(&1 + 1)
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> fun.(1)
2
iex> fun2 = &"Good #{&1}"
#Function<6.127694169/1 in :erl_eval.expr/5>
iex> fun2.("morning")
"Good morning"
위 &(&1 + 1)은 fn x -> x + 1 end와 정확히 같아요. 캡처 연산자 &에 대해 더 자세히 알고 싶다면 문서를 참고하세요.
다음으로는 지금까지 배운 데이터 타입 중 몇 가지를 다시 보면서 더 깊이 파고들어 볼게요.
더 알아보기
- case, cond, and if: 조건 분기와 패턴 매칭
- 바이너리, 문자열, 문자 리스트: 다음 장에서 다룰 데이터 타입
- Kernel 모듈:
is_function/1,is_function/2등 자동 임포트되는 함수 - IEx.Helpers:
h함수 정의 위치