Enumerable 모듈
Enumerable 모듈
Enumerable 모듈은 컬렉션 클래스에 유용한 메서드들을 제공해요. 이 메서드들은 크게 쿼리, 가져오기(Fetching), 검색/필터, 정렬, 반복 등으로 나뉘어요.
사용법
컬렉션 클래스에서 Enumerable 모듈을 쓰려면:
include로 포함하고,- 컬렉션의 연속 요소를
yield하는#each메서드를 구현해요. 거의 모든Enumerable메서드가 이#each를 호출해서 동작해요.
class Foo
include Enumerable
def each
yield 1
yield 1, 2
yield
end
end
Foo.new.each_entry{ |element| p element }
출력:
1
[1, 2]
nil
Ruby 클래스에서의 Enumerable
Ruby 코어 클래스 중 ARGF, Array, Dir, Enumerator, ENV(extend), Hash, IO, Range, Struct가 Enumerable을 포함(또는 확장)해요. 표준 라이브러리에서는 CSV, CSV::Table, CSV::Row, Set이 포함해요.
Enumerable의 거의 모든 메서드가 포함한 클래스의 #each를 호출해요:
Hash#each는 다음 키-값 쌍을 2-요소Array로yield해요.Struct#each도 다음 이름-값 쌍을 2-요소Array로yield해요.- 나머지 클래스의
#each는 컬렉션에서 다음 객체를yield해요.
메서드 한눈에 보기
쿼리(Querying): include?/member?, all?, any?, none?, one?, count, tally
가져오기(Fetching): entries/to_a, first, take, drop, take_while, drop_while, min, max, minmax, min_by, max_by, minmax_by, group_by, partition, slice_after, slice_before, slice_when, chunk, chunk_while
검색/필터(Searching & Filtering): find/detect, find_all/filter/select, find_index, reject, uniq
정렬(Sorting): sort, sort_by
반복(Iterating): each_entry, each_with_index, each_with_object, each_slice, each_cons, reverse_each
기타(Other): map/collect, filter_map, flat_map/collect_concat, grep, grep_v, reduce/inject, sum, zip, cycle, chain, lazy, compact, to_h, to_set
출처: Ruby 3.3 API
본문
all? → true or false, all?(pattern) → true or false, all? {|element| ... } → true or false
모든 요소가 주어진 기준을 만족하는지 돌려줘요. self에 요소가 없으면 true를 돌려줘요.
(1..4).all? # => true
[1, 2, nil].all? # => false
(1..4).all?(Integer) # => true
(1..4).all?(Float) # => false
(1..4).all? {|element| element < 5 } # => true
관련: any?, none?, one?.
any? → true or false, any?(pattern) → true or false, any? {|element| ... } → true or false
어떤 요소가 주어진 기준을 만족하는지 돌려줘요. self에 요소가 없으면 false.
(1..4).any? # => true
[].any? # => false
[nil, false, 0].any?(Integer) # => true
(1..4).any? {|element| element < 2 } # => true
chain(*enums) → enumerator
이 enumerator와 주어진 enumerable들로부터 생성된 enumerator 객체를 돌려줘요:
e = (1..3).chain([4, 5])
e.to_a #=> [1, 2, 3, 4, 5]
chunk {|array| ... } → enumerator
반환된 enumerator의 각 요소는 다음으로 구성된 2-요소 배열이에요:
- 블록이 돌려준 값
- 그 값이 반환된 요소와, 이후에 같은 값이 반환된 모든 요소를 담은 배열("청크")
즉 블록 반환값이 바로 앞과 다르면 새 청크가 시작돼요.
e = (0..10).chunk {|i| (i / 3).floor } # => #<Enumerator: ...>
e.next # => [0, [0, 1, 2]]
e.next # => [1, [3, 4, 5]]
특수 심볼 :_alone을 쓰면 요소를 자기만의 청크로 강제하고, :_separator나 nil을 쓰면 요소를 무시하게 해요:
a = [0, 0, 1, 1]
e = a.chunk{|i| i.even? ? :_alone : true }
e.to_a # => [[:_alone, [0]], [:_alone, [0]], [true, [1, 1]]]
chunk_while {|elt_before, elt_after| bool } → an_enumerator
인접한 두 요소 elt_before와 elt_after를 블록에 넘겨 블록이 false를 돌려주는 부분에서 청크를 나눠요. 시작 부분을 블록이 정의해요:
a = [1,2,4,9,10,11,12,15,16,19,20,21]
b = a.chunk_while {|i, j| i+1 == j }
p b.to_a #=> [[1, 2], [4], [9, 10, 11, 12], [15, 16], [19, 20, 21]]
Enumerable#slice_when은 블록이 true일 때 나눈다는 점만 달라요.
collect → an_enumerator, map → array
블록이 돌려준 객체들의 배열을 돌려줘요. collect는 map의 별칭이에요.
compact → array
모든 nil이 아닌 요소의 배열을 돌려줘요:
a = [nil, 0, nil, 'a', false, nil, false, nil, 'a', nil, 0, nil]
a.compact # => [0, "a", false, false, "a", 0]
count → integer, count(object) → integer, count {|element| ... } → integer
요소의 개수를 돌려줘요. 인자 없으면 총 개수, 인자가 있으면 ==인 개수, 블록이 있으면 블록이 참인 개수를 돌려줘요:
[0, 1, 2].count # => 3
[0, 1, 2, 1].count(1) # => 2
[0, 1, 2, 3].count {|element| element < 2} # => 2
cycle(n = nil) {|element| ...} → nil, cycle(n = nil) → enumerator
양의 정수 n과 블록으로 호출하면 각 요소로 블록을 호출한 것을 n번 반복하고 nil을 돌려줘요. n이 nil이면 영원히 반복해요. 블록이 없으면 Enumerator를 돌려줘요.
detect → an_object, find(if_none_proc = nil) {|element| ... } → object or nil
블록이 참을 돌려주는 첫 요소를 돌려줘요. 없으면 if_none_proc을 호출해 그 결과를 돌려줘요. detect는 find의 별칭이에요.
(0..9).find {|element| element > 2} # => 3
(0..9).find(proc {false}) {|element| element > 12} # => false
drop(n) → array
양의 정수 n에 대해 처음 n개를 제외한 배열을 돌려줘요:
r = (1..4)
r.drop(3) # => [4]
r.drop(50) # => []
drop_while {|element| ... } → array, drop_while → enumerator
블록이 참인 동안 요소를 버리고, 그 지점 이후의 모든 요소 배열을 돌려줘요:
(1..4).drop_while{|i| i < 3 } # => [3, 4]
each_cons(n) { ... } → self, each_cons(n) → enumerator
각 연속된 겹치는 n-튜플마다 블록을 호출하고 self를 돌려줘요:
a = []
(1..5).each_cons(3) {|element| a.push(element) }
a # => [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
each_entry(*args) {|element| ... } → self, each_entry(*args) → enumerator
각 요소로 블록을 호출하되, yield로 여러 값이 넘어오면 배열로 변환해서 넘겨요:
Foo.new.each_entry {|yielded| p yielded }
# 출력: 1 / [1, 2] / nil
each_slice(n) { ... } → self, each_slice(n) → enumerator
각 연속된 겹치지 않는 n-튜플마다 블록을 호출하고 self를 돌려줘요:
a = []
(1..10).each_slice(3) {|tuple| a.push(tuple) }
a # => [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
each_with_index(*args) {|element, i| ..... } → self, each_with_index(*args) → enumerator
각 요소와 그 인덱스로 블록을 호출하고 self를 돌려줘요:
h = {}
(1..4).each_with_index {|element, i| h[element] = i } # => 1..4
h # => {1=>0, 2=>1, 3=>2, 4=>3}
each_with_object(object) { |(*args), memo_object| ... } → object, each_with_object(object) → enumerator
각 요소와 주어진 객체를 함께 블록에 넘겨요:
(1..4).each_with_object([]) {|i, a| a.push(i**2) }
# => [1, 4, 9, 16]
entries → array, to_a(*args) → array
self의 항목들을 담은 배열을 돌려줘요. entries는 to_a의 별칭이에요.
filter → array, find_all → array, select {|element| ... } → array
블록이 선택한 요소들의 배열을 돌려줘요. select와 filter는 find_all의 별칭이에요:
(0..9).select {|element| element % 3 == 0 } # => [0, 3, 6, 9]
filter_map {|element| ... } → array, filter_map → enumerator
블록이 돌려준 참인 값들만 담은 배열을 돌려줘요:
(0..9).filter_map {|i| i * 2 if i.even? } # => [0, 4, 8, 12, 16]
find_index(object) → integer or nil, find_index {|element| ... } → integer or nil, find_index → enumerator
주어진 기준을 만족하는 첫 요소의 인덱스를 돌려주거나, 없으면 nil:
['a', 'b', 'c', 'b'].find_index('b') # => 1
['a', 'b', 'c', 'b'].find_index {|element| element.start_with?('b') } # => 1
first → element or nil, first(n) → array
첫 요소(들)를 돌려줘요. 인자 없이 첫 요소 또는 nil. 정수 n이면 처음 n개 배열:
(1..4).first # => 1
(1..4).first(2) # => [1, 2]
flat_map {|element| ... } → array, flat_map → enumerator
블록이 돌려준 객체들을 평평하게(flatten) 만든 배열을 돌려줘요. collect_concat 별칭:
[0, 1, 2, 3].flat_map {|element| [element, -element] } # => [0, 0, 1, -1, 2, -2, 3, -3]
grep(pattern) → array, grep(pattern) {|element| ... } → array
pattern === element가 참인 요소(들)을 돌려줘요. 블록이 있으면 각 매칭 요소에 블록을 적용:
a = ['foo', 'bar', 'car', 'moo']
a.grep(/ar/) # => ["bar", "car"]
a.grep(/ar/) {|element| element.upcase } # => ["BAR", "CAR"]
grep_v(pattern) → array, grep_v(pattern) {|element| ... } → array
grep과 반대로 pattern === element가 거짓인 요소(들)을 돌려줘요.
group_by {|element| ... } → hash, group_by → enumerator
블록이 돌려준 값들이 키이고, 그 키를 반환한 요소들의 배열이 값인 해시를 돌려줘요:
g = (1..6).group_by {|i| i%3 }
g # => {1=>[1, 4], 2=>[2, 5], 0=>[3, 6]}
include?(object) → true or false, member?(object) → true or false
object == element인 요소가 있는지 돌려줘요. member?의 별칭이 include?예요.
inject(symbol) → object, inject(initial_operand, symbol) → object, inject {|memo, operand| ... } → object, inject(initial_operand) {|memo, operand| ... } → object
연산자들을 symbol 이름의 메서드나 블록으로 결합해 만든 객체를 돌려줘요:
# 심볼 방식: 합
(1..4).inject(:+) # => 10
(1..4).inject(10, :+) # => 20
# 블록 방식: 제곱의 합
(1..4).inject {|sum, n| sum + n*n } # => 30
(1..4).inject(2) {|sum, n| sum + n*n } # => 32
reduce가 inject의 별칭이에요.
lazy → lazy_enumerator
Enumerator::Lazy를 돌려줘요. 이건 대부분의 Enumerable 메서드를 재정의해 열거를 미루고(as-needed) 값을 열거해요. 피타고라스 삼중조를 찾는 예시:
def pythagorean_triples
(1..Float::INFINITY).lazy.flat_map {|z|
(1..z).flat_map {|x|
(x..z).select {|y|
x**2 + y**2 == z**2
}.map {|y|
[x, y, z]
}
}
}
end
p pythagorean_triples.take(10).force # take는 lazy라 force가 필요해요
p pythagorean_triples.first(10) # first는 eager예요
map {|element| ... } → array, map → enumerator
블록이 돌려준 객체들의 배열을 돌려줘요:
(0..4).map {|i| i*i } # => [0, 1, 4, 9, 16]
collect의 별칭이 map이에요.
max → element, max(n) → array, max {|a, b| ... } → element, max(n) {|a, b| ... } → array
주어진 기준에 따라 최대인 요소를 돌려줘요. 블록이 없으면 요소의 <=>로, 블록이 있으면 블록으로 비교해요:
(1..4).max # => 4
(1..4).max(2) # => [4, 3]
%w[xxx x xxxx xx].max {|a, b| a.size <=> b.size } # => "xxxx"
max_by {|element| ... } → element, max_by(n) {|element| ... } → array, max_by → enumerator
블록이 최대값을 돌려주는 요소(들)을 돌려줘요:
(1..4).max_by {|element| -element } # => 1
(1..4).max_by(2) {|element| -element } # => [1, 2]
min → element, min(n) → array, min {|a, b| ... } → element, min(n) {|a, b| ... } → array
최소인 요소(들)을 돌려줘요. max의 비교 방향만 반대:
(1..4).min # => 1
(1..4).min(2) # => [1, 2]
%w[xxx x xxxx xx].min {|a, b| a.size <=> b.size } # => "x"
min_by {|element| ... } → element, min_by(n) {|element| ... } → array
블록이 최소값을 돌려주는 요소(들)을 돌려줘요.
minmax → [minimum, maximum], minmax {|a, b| ... } → [minimum, maximum]
최소와 최대 요소를 담은 2-요소 배열을 돌려줘요:
(1..4).minmax # => [1, 4]
%w[xxx x xxxx xx].minmax {|a, b| a.size <=> b.size } # => ["x", "xxxx"]
minmax_by {|element| ... } → [minimum, maximum], minmax_by → enumerator
블록 기준으로 최소·최대를 돌려주는 2-요소 배열:
(1..4).minmax_by {|element| -element } # => [4, 1]
none? → true or false, none?(pattern) → true or false, none? {|element| ... } → true or false
어떤 요소도 주어진 기준을 만족하지 않는지 돌려줘요:
(nil, false).none? # (예: [nil, false].none?) => true
[].none? # => true
one? → true or false, one?(pattern) → true or false, one? {|element| ... } → true or false
정확히 하나의 요소만 주어진 기준을 만족하는지 돌려줘요.
partition {|element| ... } → [true_array, false_array], partition → enumerator
블록이 참을 반환하는 요소들 배열과 나머지 요소들 배열 두 개로 나눠 돌려줘요:
p = (1..4).partition {|i| i.even? }
p # => [[2, 4], [1, 3]]
reject {|element| ... } → array, reject → enumerator
블록이 nil이나 false를 돌려주는 요소들만 담은 배열을 돌려줘요:
(0..9).reject {|i| i * 2 if i.even? } # => [1, 3, 5, 7, 9]
reverse_each(*args) {|element| ... } → self, reverse_each(*args) → enumerator
각 요소로 블록을 역순으로 호출하고 self를 돌려줘요:
a = []
%w[a b c d].reverse_each {|element| a.push(element) }
a # => ["d", "c", "b", "a"]
slice_after(pattern) → an_enumerator, slice_after { |elt| bool } → an_enumerator
요소가 패턴/블록과 일치하면 그 요소를 청크의 끝으로 삼아 청크로 나눠요:
lines = ["foo\n", "bar\\\n", "baz\n", "\n", "qux\n"]
e = lines.slice_after(/(?<!\\)\n\z/)
p e.to_a
#=> [["foo\n"], ["bar\\\n", "baz\n"], ["\n"], ["qux\n"]]
slice_before(pattern) → enumerator, slice_before {|elt| ... } → enumerator
element === pattern(또는 첫 요소)일 때 새 슬라이스를 시작하도록 요소들을 배열("슬라이스")로 나눠요:
a = %w[foo bar fop for baz fob fog bam foy]
e = a.slice_before(/ba/)
e.each {|array| p array }
# 출력: ["foo"] / ["bar", "fop", "for"] / ["baz", "fob", "fog"] / ["bam", "foy"]
slice_when {|elt_before, elt_after| bool } → an_enumerator
블록이 true를 돌려주는 부분에서 청크를 나눠요. chunk_while은 블록이 false일 때 나눈다는 점만 달라요:
a = [1,2,4,9,10,11,12,15,16,19,20,21]
b = a.slice_when {|i, j| i+1 != j }
p b.to_a #=> [[1, 2], [4], [9, 10, 11, 12], [15, 16], [19, 20, 21]]
sort → array, sort {|a, b| ... } → array
정렬된 요소들의 배열을 돌려줘요. 블록이 없으면 <=>로 비교하고, 있으면 블록 비교 결과(음수/0/양수)로 정렬해요:
%w[b c a d].sort # => ["a", "b", "c", "d"]
%w[b c a d].sort {|a, b| b <=> a } # => ["d", "c", "b", "a"]
키 계산이나 비교가 비싸면 sort_by(Schwartzian transform)가 유리해요.
sort_by {|element| ... } → array, sort_by → enumerator
블록이 돌려준 값에 따라 정렬된 배열을 돌려줘요:
a = %w[xx xxx x xxxx]
a.sort_by {|s| s.size } # => ["x", "xx", "xxx", "xxxx"]
sort_by는 내부적으로 (키, 요소) 튜플 배열을 만들어서 정렬해요.
sum(initial_value = 0) → number, sum(initial_value = 0) {|element| ... } → object
initial_value와 요소들의 합을 돌려줘요. 가능하면 가우스 합 공식 n(n+1)/2로 최적화해요:
(1..100).sum # => 5050
(1..4).sum {|i| i*i } # => 30
take(n) → array
음이 아닌 정수 n에 대해 처음 n개 요소를 돌려줘요:
(1..4).take(2) # => [1, 2]
(1..4).take(0) # => []
take_while {|element| ... } → array, take_while → enumerator
블록이 참인 동안 요소를 취해, 그 지점까지의 모든 요소 배열을 돌려줘요:
(1..4).take_while{|i| i < 3 } # => [1, 2]
tally → new_hash, tally(hash) → hash
같은 요소들의 개수를 담은 해시를 돌려줘요. 인자 해시를 주면 그 해시에 누적:
%w[a b c b c a c b].tally # => {"a"=>2, "b"=>3, "c"=>3}
to_h(*args) → hash, to_h(*args) {|element| ... } → hash
self가 2-요소 배열로 이뤄졌으면 각 배열을 키-값 쌍으로 하는 해시를 돌려줘요. 블록이 있으면 블록이 돌려준 2-요소 배열로 만들고:
[[:foo, 0], [:bar, 1], [:baz, 2]].to_h # => {:foo=>0, :bar=>1, :baz=>2}
(0..3).to_h {|i| [i, i ** 2]} # => {0=>0, 1=>1, 2=>4, 3=>9}
to_set(klass = Set, *args, &block)
enumerable 객체에서 집합을 만들어요. 쓰려면 require "set"이 필요해요.
uniq → array, uniq {|element| ... } → array
블록이 없으면 고유한 요소만 담은 새 배열을, 블록이 있으면 블록이 고유한 값을 반환하는 요소만 담은 배열을 돌려줘요:
%w[a b c c b a a b c].uniq # => ["a", "b", "c"]
a = [0, 1, 2, 3, 4, 5, 5, 4, 3, 2, 1]
a.uniq {|i| i.even? ? i : 0 } # => [0, 2, 4]
zip(*other_enums) → array, zip(*other_enums) {|array| ... } → nil
각 other_enums의 n번째 요소와 self의 n번째 요소들을 묶은 2차원 배열을 돌려줘요. other_enums가 더 작으면 nil로 채우고, 더 크면 넘친 뒤쪽은 무시해요:
a = [:a0, :a1, :a2, :a3]
b = [:b0, :b1, :b2, :b3]
a.zip(b) # => [[:a0, :b0], [:a1, :b1], [:a2, :b2], [:a3, :b3]]
더 알아보기
Enumerable을 직접 구현한 컬렉션 클래스에서는each하나만 올바르게 구현하면 거의 모든Enumerable메서드를 공짜로 얻을 수 있어요. 이게 Ruby 컬렉션 설계의 핵심이에요.chunk,slice_before/after/when,chunk_while은 연속 흐름(파일, 로그, 스트림)을 처리할 때 정말 강력해요.- 무한한 데이터를 다룰 땐
lazy를 쓰면 필요한 만큼만 계산할 수 있어요.