Code

Elixir – 05: case, cond and if

February 10, 2016

author:

Array

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/1
    • is_binary/1
    • is_bitstring/1
    • is_boolean/1
    • is_float/1
    • is_function/1
    • is_function/2
    • is_integer/1
    • is_list/1
    • is_map/1
    • is_nil/1
    • is_number/1
    • is_pid/1
    • is_port/1
    • is_reference/1
    • is_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)"

마지막으로 condnil, false를 제외한 모든 값을 참으로 평가합니다. 이건 논리 연산자와 동일하네요. 🙂

ifunless

Elixir에서는 casecond 이외에도, 하나의 조건을 확인하기 위한 if/2unless/2도 제공됩니다.

iex> if true do
...>   "This works!"
...> end
"This works!"
iex> unless true do
...>   "This will never be seen"
...> end
nil

만약 if/2의 조건이 falsenil로 평가된다면 블럭은 실행되지 않고 nil을 반환합니다. unless/2에서는 반대의 상황이 발생할 거구요.

물론 else 블럭도 있습니다.

iex> if nil do
...>   "This won't be seen"
...> else
...>   "This will"
...> end
"This will"

이상하달까, 흥미로운 점은 if/2unless/2가 수많은 언어들과는 다르게 매크로의 형태로 구현되었다는 점입니다. if/2의 코드를 the Kernel module docs에서 확인할 수 있습니다. 따라가서 깃헙까지 확인해 보았는데, defmacro를 써서 선언한다는 것만 알겠네요. 신비한 Erlang…

추가로 Kernel 모듈은 기본적인 +/2is_function/2 같은 함수들이 포함되어 있으며, 기본으로 로드됩니다.

do/end blocks

여기까지 case, cond, if, unless를 배웠습니다. 지금까지 본 예제들을 잘 살펴보면 언제나 do/end라는 블럭으로 실행할 코드를 포함하고 있었는데요. 사실 다음과 같이 작성할 수도 있습니다.

iex> if true, do: 1 + 2
3

truedo:의 사이에 쉼표를 사용합니다. 이는 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

Reference

Array