Elixir – 05: case, cond and if
Elixir Tutorial 시리즈입니다. 거의 대부분은 튜토리얼의 한글 번역에 가깝습니다만, 생략되거나 추가로 주석을 달거나 하는 부분이 많습니다. 원문은 최하단의 링크를 참고하세요.
Elixir – 05: case, cond and if
Elixir의 Flow Control에 대해서 알아봅시다 =3
case
Ruby의 그 case 맞습니다. 다만 when을 쓰지 않는다는 점이 다르네요. 가장 특이한 부분은 조건을 확인할 때 매칭을 사용한다는 점입니다.
iex> case {1, 2, 3} do
...> {4, 5, 6} ->
...> "This clause won't match"
...> {1, x, 3} ->
...> "This clause will match and bind x to 2 in this clause"
...> _ ->
...> "This clause would match any value"
...> end
"This clause will match and bind x to 2 in this clause"
예제가 모든걸 설명하고 있습니다. 만약 매칭하지 않는 것을 처리하고 싶다구요? 저번에 배웠던 핀 연산자를 쓰면 됩니다.
iex> x = 1
1
iex> case 10 do
...> ^x -> "Won't match"
...> _ -> "Will match"
...> end
"Will match"
이예이.
조건절에서 가드를 통해 추가 조건을 줄 수도 있습니다.
iex> case {1, 2, 3} do
...> {1, x, 3} when x > 0 ->
...> "Will match"
...> _ ->
...> "Would match, if guard condition were not satisfied"
...> end
"Will match"
Expressions in guard clauses
Elixir에서는 가드에서 다음의 표현식들을 기본적으로 사용할 수 있습니다.
- 비교 연산자 (
==,!=,===,!==,>,>=,<,<=) - 진리 연산자 (
and,or,not) - 산술 연산자 (
+,-,*,/) - 산술 단항 연산자 (
+,-) - 이항 연결 연산자
<> - 우측에 범위나 리스트를 가지고 있는
in연산자 - 다음의 타입 검사 함수:
is_atom/1is_binary/1is_bitstring/1is_boolean/1is_float/1is_function/1is_function/2is_integer/1is_list/1is_map/1is_nil/1is_number/1is_pid/1is_port/1is_reference/1is_tuple/1
- 그리고 기타:
abs(number)binary_part(binary, start, length)bit_size(bitstring)byte_size(bitstring)div(integer, integer)elem(tuple, n)hd(list)length(list)map_size(map)node()node(pid | ref | port)rem(integer, integer)round(number)self()tl(list)trunc(number)tuple_size(tuple)
추가로 커스텀 가드를 만들 수 있습니다. 예를 들어, Bitwise 모듈은 가드를 함수와 연산자로 정의하고 있습니다 (bnot, ~~~, band,
&&&, bor, |||, bxor, ^^^, bsl, <<<, bsr, >>>).
특이한 점은 진리 연산자(and, or, not)는 사용 가능하지만 일반 논리 연산자(&&, ||, !)는 못씁니다. ;ㅅ;
한가지 더 특이한 점은, 가드 내에서 발생한 에러는 외부로 던져지지 않고 그저 가드가 실패하도록 만든다는 점입니다.
iex> hd(1)
** (ArgumentError) argument error
:erlang.hd(1)
iex> case 1 do
...> x when hd(x) -> "Won't match"
...> x -> "Got: #{x}"
...> end
"Got 1"
하나도 매칭되는 조건절이 없다면 에러가 발생합니다.
iex> case :ok do
...> :error -> "Won't match"
...> end
** (CaseClauseError) no case clause matching: :ok
익명함수에서는 다수의 명령, 가드를 사용할 수 있습니다.
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
아직 함수의 구조가 어떻게 되는 건지는 모르니 확신할 수 없지만 이쯤 되면 ->가 return의 역할을 하는게 아닌가 추측됩니다. 그렇다고 한다면 case 절 내에서 ->로 끝내는 것도 납득할 수 있는 상황.
돌아와서 이러한 익명함수에서 조심해야 하는 부분은 각 절들은 동일한 갯수를 동일하게 맞추어주어야 한다는 점입니다. 그렇지 않으면 에러가 발생합니다.
cond
case는 어떤 값 하나를 다양한 방식으로 매칭할 때에 유용합니다. 하지만 많은 환경에서는 다른 종류의 조건에 따라 각각을 평가하고 그 중 만족하는 하나의 조건에 따른 식을 평가해야 합니다. 그러한 경우 cond는 하나의 선택지가 될 수 있습니다.
iex> cond do
...> 2 + 2 == 5 ->
...> "This will not be true"
...> 2 * 2 == 3 ->
...> "Nor this"
...> 1 + 1 == 2 ->
...> "But this will"
...> end
"But this will"
이는 많은 언어에서 사용하는 else if와 동치입니다.
어떤 조건도 만족하지 못한다면 cond 역시 에러를 던집니다. 그렇기 때문에 항상 조건을 만족하는 마지막 조건을 추가할 필요가 있습니다.
iex> cond do
...> 2 + 2 == 5 ->
...> "This is never true"
...> 2 * 2 == 3 ->
...> "Nor this"
...> true ->
...> "This is always true (equivalent to else)"
...> end
"This is always true (equivalent to else)"
마지막으로 cond는 nil, false를 제외한 모든 값을 참으로 평가합니다. 이건 논리 연산자와 동일하네요. 🙂
if와 unless
Elixir에서는 case와 cond 이외에도, 하나의 조건을 확인하기 위한 if/2와 unless/2도 제공됩니다.
iex> if true do
...> "This works!"
...> end
"This works!"
iex> unless true do
...> "This will never be seen"
...> end
nil
만약 if/2의 조건이 false나 nil로 평가된다면 블럭은 실행되지 않고 nil을 반환합니다. unless/2에서는 반대의 상황이 발생할 거구요.
물론 else 블럭도 있습니다.
iex> if nil do
...> "This won't be seen"
...> else
...> "This will"
...> end
"This will"
이상하달까, 흥미로운 점은 if/2나 unless/2가 수많은 언어들과는 다르게 매크로의 형태로 구현되었다는 점입니다. if/2의 코드를 the Kernel module docs에서 확인할 수 있습니다. 따라가서 깃헙까지 확인해 보았는데, defmacro를 써서 선언한다는 것만 알겠네요. 신비한 Erlang…
추가로 Kernel 모듈은 기본적인 +/2나 is_function/2 같은 함수들이 포함되어 있으며, 기본으로 로드됩니다.
do/end blocks
여기까지 case, cond, if, unless를 배웠습니다. 지금까지 본 예제들을 잘 살펴보면 언제나 do/end라는 블럭으로 실행할 코드를 포함하고 있었는데요. 사실 다음과 같이 작성할 수도 있습니다.
iex> if true, do: 1 + 2
3
true와 do:의 사이에 쉼표를 사용합니다. 이는 Elixir에서 각 인자를 구분할 때에 쉼표를 쓰기 때문입니다. 우리는 이 문법을 키워드 목록을 사용하고 있다고 표현합니다. 같은 방식으로 else도 넘길 수 있죠.
iex> if false, do: :this, else: :that
:that
do/end 블럭은 키워드를 사용하는 것에 비해서 문법적인 편리함을 가져다줍니다. 바로 do/end 블럭에서 이전의 블럭 인수의 사이에 쉼표를 쓰지 않아도 된다는 점입니다. 이를 통해 블럭으로 코드를 작성할 때 코드를 보기좋게 유지할 수 있게 됩니다. 다음의 두 예제는 동등합니다.
iex> if true do
...> a = 1 + 2
...> a + 10
...> end
13
iex> if true, do: (
...> a = 1 + 2
...> a + 10
...> )
13
do/end 블럭을 사용할 때에 주의해야할 점은 이것들이 언제나 가장 바깥에 있는 함수 호출에 대응한다는 점입니다. 예를 들자면 다음의 코드는,
iex> is_number if true do
...> 1 + 2
...> end
** (RuntimeError) undefined function: if/1
이것과 같습니다.
iex> is_number(if true) do
...> 1 + 2
...> end
** (RuntimeError) undefined function: if/1
그러므로 Elixir는 if/1을 호출하려고 시도하고, 에러를 던집니다. 이 문제는 괄호를 명시적으로 사용하여 회피할 수 있습니다.
iex> is_number(if true do
...> 1 + 2
...> end)
true
