Powered by AppSignal & Oban Pro

처음배우는엘릭서프로그래밍

처음배우는엘릭서프로그래밍/10-4.livemd

처음배우는엘릭서프로그래밍

10.4 컴프리헨션문

함수형 코드를 작성하다 보면 컬렉션에 매핑 함수를 적용하거나 항목을 필터링하는 일이 많다. 컴프리헨션 문은 컬렉션을 다루는 일반적인 작업에 사용 가능한 단축 문법으로, 활용 하면 작업을 좀더 쉽게 수행하고 코드의 가독성도 높일 수 있다.

컴프리헨션 문에 담긴 아이디어는 매우 간단하다. 컬렉션이 하나 이상 있을 때 각 컬렉션의 값으로 만들 수 있는 모든 조합을 계산한 뒤, 조건에 따라 항목을 필터링해 남은 값으로 새 컬렉션을 생성한다. 문법은 겉보기에는 쉬워보인다.

result = for 제너레이터 또는 필터... [, into: 값], do: 표현식

자세한 내용으로 들어가기 전에 기본적인 예를 몇 가지 살펴보자.

for x <- [1, 2, 3, 4, 5], do: x * x
for x <- [1, 2, 3, 4, 5], x < 4, do: x * x

제너레이터를 사용해 컬렉션에서 값을 어떻게 추출할지를 정할 수 있다. 제너레이터의 문법은 다음과 같다.

패턴 <- 순회 가능한 값

패턴에 매칭된 변수들은 컴프리헨션 문의 다른 부분에서 (do 블록 내에서도) 사용할 수 있다. 예를 들어, x <- [1,2,3]은 우선 x=1인 상태로 컴프리헨션의 나머지 부분을 실행하고, 그 다음에 x=2인 상태로 나머지 부분을 실행한다. 제너레이터가 2개이면 각각을 중첩해 적용한다.

x <- [1,2], y <- [5,6]

제너레이터를 이와 같이 작성하면 x=1, y=5 x=1, y=6 x=2, y=6 순서로 컴프리헨션 문의 나머지 부분을 수행하게 된다. 이 값들을 do 블록 안에서 x, y 변수로 사용할 수 있다.

for x <- [1, 2], y <- [5, 6], do: x * y
for x <- [1, 2], y <- [5, 6], do: {x, y}

앞에서 받은 변수를 뒤에 있는 제너레이터에서 사용할 수도 있다.

min_maxes = [{1, 4}, {2, 3}, {10, 15}]
for {min, max} <- min_maxes, n <- min..max, do: n

필터는 참 혹은 거짓을 반환하는 명제다. 필터는 컴프리헨션의 뒷부분에서 원하는 값만을 사용하도록 문지기 역활은 한다. 필터 조건이 거짓이면 컴프리헨션 문이 더 실행되지 않고 다음 값으로 넘어간다.

예를 들어보자. 다음 코드는 1부터 8까지의 리스트로 만들 수 있는 모든 숫자 쌍중에 서로 곱한 값이 10의 배수인 것만을 찾아 반환한다. 코드의 컴프리헨션 문은 숫자 쌍을 순회하기 위한 제너레이터 두 개와 필터 두 개를 사용한다. 첫 번째 필터는 첫 번째 수가 두 번째 수보다 작지 않은 숫자 쌍을 골라내며, 두 번째 필터는 두 수의 곱이 10의 배수인지 확인한다.

first8 = [1, 2, 3, 4, 5, 6, 7, 8]
for x <- first8, y <- first8, x >= y, rem(x * y, 10) == 0, do: {x, y}

이 컴프리헨션 문은 x=1, y=1 -> x=1, y=2 -> ...와 같은 식으로 총 64회 반복된다. 하지만 첫 번쨰 필터가 xy보다 작은 경우를 걸러내므로 두 번째 필터는 36회만 실행된다. 제너레이터의 좌변에는 패턴이 오므로 데이터의 구조를 분해해 사용할 수도 있다. 다음 코드는 컴프리핸션 문을 이용해 키워드 리스트의 키와 값을 서로 바꾸는 예다.

reports = [dallas: :hot, minneapolis: :cold, dc: :muggy, la: :smoggy]
for {city, weather} <- reports, do: {weather, city}

10.4.1 비스트스트링에 컴프리헨션 사용하기

연속된 비트인 비트스트링(그리고 비트스트링의 확장인 바이너리나 문자열) 역시 2진수의 컬렉션으로 생각할 수 있다. 따라서 비트스트링에도 컴프리헨션 문을 사용할 수 있다는 사실은 어쩌면 당연하다. 하지만 문법은 그리 당연함과는 다소 거리가 있다.

for <<ch <- "hello">>, do: ch
for <<ch <- "hello">>, do: <<ch>>

제너레이터가 바이너리를 의미하는 << >>기호로 감싸여 있다. 첫 번째 컴프리헨션 문에서는 do 블록이 각 문자에 대한 정수 코드를 반환하므로, 결과로 반환되는 리스트는 [104, 101, 108, 111]이다. IEx에서 이 리스트는 'hello로 표시된다. 두 번째 컴프리헨션 문은 각각의 정수 코드를 문자열로 바꿔 반환하므로 결과는 한 글자짜리 문자열들의 리스트가 된다.

제너레이터의 좌변이 패턴이므로 바이너리 패턴 매칭도 사용할 수 있다. 다음 코드는 문자열을 이루는 각 문자의 정수 코드를 8진수로 바꾼다.

for <<(<<b1::size(2), b2::size(3), b3::size(3)>> <- "hello")>>,
  do: "0#{b1}#{b2}#{b3}"

10.4.2 컴프리헨션 문의 스코프

컴프리헨션 문 내에서 할당하는 변수는 해당 컴프리헨션 문 안에서만 사용할 수 있다. 외부 변수의 값에는 영향을 미치지 않는다.

name = "Dave"
for name <- ["cat", "dog"], do: String.upcase(name)
name

10.4.3 컴프리헨션 문이 반환하는 값

지금까지 본 컴프리헨션 문은 라스트를 반환했으며, 리스트에 포함되는 값은 컴프리헨션 문의 각 반복에서 do 블록이 반환하는 값이었다. info: 파라미터를 사용해 이 동작을 바꿀 수 있다. into: 파라미터는 결과를 저장할 컬렉션을 받는다. 예를 들어 결과를 맵으로 받을 수도 있다.

for x <- ~w{ cat dog }, into: %{}, do: {x, String.upcase(x)}

Map.new를 쓰면 의미가 좀 더 명확하게 전달될 것이다.

for x <- ~w{ cat dog }, into: Map.new(), do: {x, String.upcase(x)}

컬렉션이 꼭 비어 있을 필요는 없다

for x <- ~w{ cat dog }, into: %{"ant" => "ANT"}, do: {x, String.upcase(x)}

into: 옵션은 Callectable 프로토콜을 구현한 값을 받는다(프로코톨은 서로 다른 자료형 사이의 공통적인 동작을 정의한 것으로 24장에서 다시 살펴본다). Collectable 프로토콜을 구현한 타입에는

  • 리스트
  • 바이너리
  • 함수
  • 파일
  • 해시 딕셔너리
  • 해시 셋
  • IO 스트림

등이 있다. 따라서 다음과 같은 코드도 작성할 수 있다.

for x <- ~w{ cat dog }, into: IO.stream(:stdio, :line), do: "<<#{x}>>\n"

연습문제

1번

2부터 n까지의 소수 리스트를 반환하는 함수를 만들어보자. 7장 마지막 연습문제에서 작서한 span 함수와 리스트 컴프리헨션 문을 이용하자.

defmodule Mylist do
  def span(from, to) when from <= to do
    [from | span(from + 1, to)]
  end

  def span(from, to) when from > to do
    []
  end

  def prime(n) do
    for i <- span(2, n), is_prime(i), do: i
  end

  defp is_prime(n) do
    for i <- span(2, n - 1), rem(n, i) == 0, do: false
    true
  end
end

IO.inspect(Mylist.prime(20))

2번

Progmatic Bookshelf 출판사는 사무실이 텍사스주(TX)와 노스캐롤라이나주(NC)에 있으며 두 주로 배송되는 주문 건에는 소비세가 부과된다. 소비세율은 키워드 리스트로 주어진다.

tax_rates = [NC: 0.075, TX: 0.08]

주문 리스트는 다음과 같다

orders = [
  [ id: 123, ship_to: :NC, net_amount: 100.00 ],
  [ id: 124, ship_to: :OK, net_amount: 35.50 ],
  [ id: 125, ship_to: :TX, net_amount: 24.00 ],
  [ id: 126, ship_to: :TX, net_amount: 44.00 ],
  [ id: 127, ship_to: :NC, net_amount: 25.00 ],
  [ id: 128, ship_to: :MA, net_amount: 10.00 ],
  [ id: 129, ship_to: :CA, net_amount: 102.00 ],
  [ id: 130, ship_to: :NC, net_amount: 50.00 ],
]

두 리스트를 받아 주문 리스트의 복사본을 반환하는 함수를 구현하자. 반환되는 주문 리스트의 각 항목에는 금액(net_amount)에 소비세를 더한 total_amount 필드를 추가해야 한다. 두 주 외의 다른 곳으로 배송되는 주문에는 소비세가 부과되지 않는다.