컴프리헨션
컴프리헨션 (Comprehensions)
어떤 Enumerable을 순회하면서 일부 결과는 걸러내고, 값을 매핑해 다른 리스트로 만드는 일은 Elixir에서 아주 흔해요. 컴프리헨션은 이런 흔한 작업들을 for 특수 형식(special form)으로 묶어주는 문법 설탕(syntactic sugar)이에요. 이번 장에서 그 쓰임을 천천히 살펴볼게요.
출처: Elixir 공식 가이드
본문
Elixir에서는 Enumerable을 순회하면서 일부 결과를 걸러내고, 값을 다른 리스트로 매핑하는 일이 흔해요. 컴프리헨션은 이런 공통 작업들을 for 특수 형식으로 묶어주는 문법 설탕이에요.
예를 들어 정수 리스트를 각각의 제곱값으로 매핑할 수 있어요.
iex> for n <- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]
컴프리헨션은 생성기(generator), 필터(filter), 수집 대상(collectable) 세 부분으로 이루어져요.
생성기와 필터 (Generators and filters)
위 표현식에서 n <- [1, 2, 3, 4]가 생성기예요. 컴프리헨션에서 사용할 값을 말 그대로 "생성"해주죠. 생성기 표현식의 오른쪽에는 어떤 Enumerable이든 올 수 있어요.
iex> for n <- 1..4, do: n * n
[1, 4, 9, 16]
생성기 표현식은 왼쪽에서 패턴 매칭도 지원해요. 매치되지 않는 패턴은 모두 무시되죠. 범위 대신, 키가 :good 또는 :bad라는 원자인 키워드 리스트가 있고 :good 값들의 제곱만 계산하고 싶다고 상상해 볼게요.
iex> values = [good: 1, good: 2, bad: 3, good: 4]
iex> for {:good, n} <- values, do: n * n
[1, 4, 16]
패턴 매칭 대신 필터를 써서 특정 원소들을 선택할 수도 있어요. 예를 들어 3의 배수만 고르고 나머지는 버릴 수 있죠.
iex> for n <- 0..5, rem(n, 3) == 0, do: n * n
[0, 9]
컴프리헨션은 필터 표현식이 false나 nil을 반환하는 모든 원소를 버리고, 그 외의 값은 모두 선택해요.
컴프리헨션은 보통 Enum이나 Stream 모듈의 동등한 함수를 쓰는 것보다 훨씬 간결한 표현을 제공해요. 게다가 컴프리헨션은 여러 개의 생성기와 필터도 허용해요. 디렉터리 리스트를 받아 그 디렉터리들의 각 파일 크기를 구하는 예시를 볼게요.
dirs = ["/home/mikey", "/home/james"]
for dir <- dirs,
file <- File.ls!(dir),
path = Path.join(dir, file),
File.regular?(path) do
File.stat!(path).size
end
여러 생성기를 쓰면 두 리스트의 카테시안 곱(Cartesian product)도 계산할 수 있어요.
iex> for i <- [:a, :b, :c], j <- [1, 2], do: {i, j}
[a: 1, a: 2, b: 1, b: 2, c: 1, c: 2]
마지막으로, 컴프리헨션 안에서의 변수 할당은(생성기 안이든 필터 안이든 블록 안이든) 컴프리헨션 밖으로 반영되지 않는다는 점을 꼭 기억해 두세요.
비트스트링 생성기 (Bitstring generators)
비트스트링 생성기도 지원되는데, 비트스트링 스트림을 순회해야 할 때 아주 유용해요. 아래 예시는 바이너리에서 각각의 빨강·초록·파랑 값을 가진 픽셀 리스트를 받아, 각각을 세 요소의 튜플로 변환해요.
iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]
비트스트링 생성기는 "일반적인" Enumerable 생성기와 섞을 수 있고, 필터도 지원해요.
:into 옵션 (The :into option)
지금까지의 예시에서는 모든 컴프리헨션이 결과로 리스트를 반환했어요. 하지만 컴프리헨션에 :into 옵션을 넘기면 결과를 다른 데이터 구조에 넣을 수도 있어요.
예를 들어 비트스트링 생성기에 :into 옵션을 함께 쓰면 문자열에서 모든 공백을 쉽게 제거할 수 있어요.
iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"
집합(set), 맵(map), 그리고 다른 딕셔너리들도 :into 옵션에 줄 수 있어요. 일반적으로 :into는 Collectable 프로토콜을 구현한 어떤 구조든 받아들여요.
:into의 흔한 쓰임 중 하나는 맵의 값들을 변환하는 거예요.
iex> for {key, val} <- %{"a" => 1, "b" => 2}, into: %{}, do: {key, val * val}
%{"a" => 1, "b" => 4}
스트림을 쓰는 예시도 하나 더 들어볼게요. IO 모듈이 스트림(Enumerable이자 Collectable인)을 제공하므로, 입력된 내용의 대문자 버전을 그대로 되돌려주는 에코 터미널을 컴프리헨션으로 구현할 수 있어요.
iex> stream = IO.stream(:stdio, :line)
iex> for line <- stream, into: stream do
...> String.upcase(line) <> "\n"
...> end
이제 터미널에 아무 문자열이나 입력하면 같은 값이 대문자로 출력되는 걸 볼 수 있어요. 다만 안타깝게도 이 예시는 IEx 셸을 컴프리헨션 안에 가둬 버리니, 빠져나오려면 Ctrl+C를 두 번 눌러야 해요. :)
기타 옵션 (Other options)
컴프리헨션은 :reduce나 :uniq 같은 다른 옵션들도 지원해요. 컴프리헨션에 대해 더 배우고 싶다면 아래 자료를 참고하세요.
- Elixir 문서의 공식 참조
- Mitchell Hanberg의 Elixir 컴프리헨션 종합 가이드
더 알아보기
- Enumerables and Streams (이전 장)
- Protocols (다음 장)
for특수 형식 (Elixir 공식 문서)