Stream — 스트림을 만들고 조합하는 함수들
Stream — 스트림을 만들고 조합하는 함수들
Elixir에서 큰 컬렉션(심지어 무한한 컬렉션)을 다룰 때, Enum으로 한 번에 몽땅 계산하기보다는 스트림(stream) 을 쓰는 편이 훨씬 유리할 때가 많아요. Stream 모듈은 그런 스트림을 만들고 조합하는 함수들로 가득한 모듈이에요.
본문
스트림은 조합 가능하고(composable) 게으른(lazy) 열거형(enumerable)이에요. 열거 중에 원소를 하나씩 생성하는 열거형이라면 뭐든 스트림이라 부를 수 있죠. 예를 들어 Elixir의 Range도 스트림이에요.
iex> range = 1..5
1..5
iex> Enum.map(range, &(&1 * 2))
[2, 4, 6, 8, 10]
이 예시에서 range를 매핑하며 열거되는 원소는 열거하는 동안에 하나씩 만들어졌어요. 그런데 Stream 모듈을 쓰면 실제 열거를 일으키지 않고 매핑을 걸어둘 수 있어요.
iex> range = 1..3
iex> stream = Stream.map(range, &(&1 * 2))
iex> Enum.map(stream, &(&1 + 1))
[3, 5, 7]
여기서 눈여겨볼 점이 있어요. 우리는 range로 시작해 각 원소를 2배로 만드는 스트림을 만들었는데, 이 시점까지는 아무 계산도 일어나지 않았어요. 나중에 Enum.map/2를 호출해야 비로소 범위의 각 원소를 실제로 열거하며 2배로 곱하고 1을 더해요. 이렇게 Stream의 함수는 게으르고(lazy), Enum의 함수는 즉시 처리하고(eager) 라고 말해요.
게으른 특성 덕분에 스트림은 크거나 무한한 컬렉션을 다룰 때 특히 유용해요. Enum으로 여러 연산을 체이닝하면 그때그때 중간 리스트가 생성되지만, Stream은 나중에 한꺼번에 실행되는 계산의 레시피만 만들어 두거든요. 나중에 스트림을 소비할 때(대부분 Enum 모듈의 함수로) 스트림이 원소를 하나씩 내뱉어요.
Enum과 실행 순서의 차이
Enum으로 세 번 매핑하면 리스트가 그때마다 다시 열거돼요.
1..3
|> Enum.map(&IO.inspect/1)
|> Enum.map(&(&1 * 2))
|> Enum.map(&IO.inspect/1)
이 경우 리스트는 세 번 열거돼요. 반면 스트림으로 같은 흐름을 만들면:
stream = 1..3
|> Stream.map(&IO.inspect/1)
|> Stream.map(&(&1 * 2))
|> Stream.map(&IO.inspect/1)
Enum.to_list(stream)
결과는 같지만, 원소가 한 번만 열거돼요. 스트림은 첫 원소를 찍고, 그 2배를 찍고, 다음 원소로 넘어가는 방식이라서요. Stream.map/2를 여러 번 불러도 스트림을 계속 조합하며 게으름을 유지하고, 실제 계산은 Enum 함수를 부를 때만 일어나요.
Enum처럼 이 모듈의 함수들도 선형 시간(linear time) 안에 동작해요. 스트림을 전부 훑어야 하니까 스트림이 길수록 시간도 비례해 늘어나는 건 자연스러운 일이에요.
스트림 만들기
Elixir 표준 라이브러리에도 스트림을 돌려주는 함수가 여럿 있어요.
IO.stream/2— 입력 줄을 하나씩 스트리밍URI.query_decoder/1— 쿼리 문자열을 쌍 단위로 디코딩
이 모듈은 또 Stream.cycle/1, Stream.unfold/2, Stream.resource/3처럼 스트림을 만드는 편의 함수도 다양하게 제공해요.
스트림 struct를 직접 검사하지 마세요
이 모듈의 함수 중 일부는 Stream struct를 돌려주기도 하지만, 절대 명시적으로 Stream struct를 검사하면 안 돼요. 스트림은 IO.Stream, File.Stream, 심지어 Range처럼 여러 모양으로 존재할 수 있거든요. 이 모듈의 함수는 열거형을 돌려주기만 보장하며, 내부 구현(struct나 익명 함수)은 언제든 바뀔 수 있어요.
특정 타입을 검사하는 대신, 열거형이 있다고 전제하고 단정하는 코드(assertive code) 를 쓰면서 필요에 따라 Enum이나 Stream의 함수를 알맞게 사용하는 게 올바른 방법이에요.
더 알아보기
- 스트림 만들기:
Stream.cycle/1,Stream.unfold/2,Stream.resource/3,Stream.iterate/2,Stream.repeatedly/1 - 변환:
Stream.map/2,Stream.filter/2,Stream.flat_map/2,Stream.chunk_every/2 - 중간 처리:
Stream.take/2,Stream.drop/2,Stream.concat/2,Stream.zip/2 - 비동기 처리:
Stream.async/1,Stream.async_stream/3 - 소비(실행):
Enum.to_list/1,Enum.map/2등Enum모듈의 함수