오늘은 배열에서 조금 특수한 목적을 위해 반복 처리를 해야할 때 사용할 수 있는 몇 가지 유용한 메서드에 대해 알아보겠다.
그중 첫 번째가 map 메서드인데 map 메서드는 호출 시 전달된 블록을 각각의 요소에 대해 실행한 결괏값으로 새 배열을 만들어 반환한다.
아래 그림을 보면 map 메서드를 사용하는 몇 가지 예가 보인다.
첫 번째는 알파벳 소문자를 요소로 갖는 배열에 map 메서드를 호출하여 각 요소(알파벳)의 대문자를 요소로 갖는 새 배열을 받았고, 두 번째는 정수를 요소로 갖는 배열에 대해 map 메서드를 호출하여 각 요소(정수)의 문자열 값을 요소로 갖는 새 배열을 받았으며, 마지막 세 번째는 똑같이 정수를 요소로 갖는 배열에 대해 map 메서드를 호출하여 각 요소(정수)의 제곱 값을 요소로 갖는 새 배열을 받았다.
그 다음 소개할 메서드는 reduce 로 이 메서드는 배열의 요소를 하나의 값으로 축약할 때 사용한다.
쉬운 예로 정수를 요소로 갖는 배열에서 모든 요소(정수)의 합계를 구하거나 가장 작은 값 또는 가장 큰 값을 구할 때 사용할 수 있다.
reduce 메서드에 전달하는 블록에는 변수를 두 개 선언해야 하는데 첫 번째 변수는 직전 블록 실행의 결괏값을 다음 블록 실행에서 사용하기 위해 필요하고, 두번째 변수는 배열의 요소를 하나씩 건네 받기 위해 필요하다. 처음 블록을 실행할 때는 직전 블록 실행의 결괏값이 없으므로 초깃값을 첫 번째 블록 파라미터에 전달해야 하는데, reduce 메서드 호출 시 인수를 주면 그 인수가 초깃값으로 사용되고 인수가 없으면 첫 번재 요소가 초깃값으로 사용된다.
아래 그림의 첫 번째 reduce 메서드 예는 인수를 주지 않았기 때문에 첫 블록 실행 때 블록 파라미터 a 와 b 에 각각 첫 번째 요소의 값(1)과 두 번째 요소의 값(2)이 전달되었고, 그 다음 블록 실행 때 변수 a 에는 직전 블록 실행의 결괏값인 3(1 + 2)이 변수 b 에는 세 번째 배열 요소의 값인 3 이 전달되었다.
두 번째 reduce 메서드 예는 인수로 10을 주었기 때문에 첫 번째 블록 실행 때 블록 파라미터 a 에는 10이 b 에는 배열의 첫 번째 요소의 값인 1 이 전달되었다.
그 아래는 reduce 메서드를 사용하여 1부터 5까지의 정수 배열에서 최솟값 1과 최댓값 5를 각각 구해 보았다.
물론 배열에는 합계 그리고 최솟값과 최댓값을 쉽게 계산해 주는 sum, min, max 메서드를 이미 가지고 있다. 직접 테스트해 보길 바란다.
그리고 map 메서드와 reduce 메서드를 함께 사용하면 전처리 후 축약하는 유형의 작업들을 보다 쉽게 처리할 수 있는데
간단한 예로는 정수로 된 배열이 있을 때 각 정수의 제곱의 합계를 구한다거나 조금 복잡하지만 더 실용적인 예로는 여러 개의 파일에서 모든 단어의 출현 빈도수를 계산하는 작업 등이 있을 수 있다.
아래 그림을 보면 첫 번째 예는 map 과 reduce 를 사용하는 것보다 sum 메서드를 사용하면 훨씬 간단함을 알 수 있다.
sum 메서드는 합계를 구하는 특정한 작업에 사용하고 map 과 reduce 는 여러 다양한 작업에 범용적으로 사용할 수 있는 메서드라고 생각하자.
실제 map 과 reduce 를 사용하여 sum 메서드를 간단히 만들어 볼 수도 있다.
이제 map 과 reduce 메서드를 함께 사용하여 아래의 영어 동요 가사가 적힌 두 개의 텍스트 파일에서 모든 단어의 출현 빈도수를 계산해 보자.
def count_word(file)
word_h = Hash.new(0)
File.foreach(file) do |str|
str.split.map { |word| word.downcase.delete(".,!?") }.each { |word| word_h[word] += 1 }
end
word_h
end
files = ["sample1.txt", "sample2.txt"]
word_h = files.map do |file|
count_word(file)
end.reduce do |a, b|
a.merge(b) { |k, old_v, new_v| old_v + new_v }
end
우선 count_word 라는 메서드를 하나 만들자. 이 메서드는 인수로 하나의 파일명을 받아 해당 파일의 내용에서 단어 빈도수를 계산한 결과를 해시에 담아 돌려준다.
파일 내용을 한 라인씩 읽어와 공백 문자를 기준으로 나누는데 이렇게 나눠진 단어를 그대로 카운트하지 않고 map 메서드를 통해 먼저 필요한 처리를 했다.
대소문자 차이로 서로 다른 단어로 인식되지 않도록 했고 또한 단어에 붙은 구두점들을 제거하였다.
그 아래 코드는 두 개의 파일명을 담은 배열에 대해 먼저 map 메서드를 사용하여 단어의 출현 빈도를 담은 해시 배열을 돌려받았고 이어서 reduce 메서드를 통해 배열 내 모든 해시 값을 하나로 합친 후 반환했다.
여기서 해시와 해시를 합치기 위해 해시의 merge 메서드를 사용했는데 대상 해시와 인수로 넘긴 해시의 키가 서로 겹치지 않는다면 문제될 게 없지만, 키가 겹친다면 전달한 블록의 결괏값이 해당 키에 대한 값으로 최종 해시에 포함되게 된다. 만약 블록을 전달하지 않고 merge를 호출했는데 '키' 가 겹친다면 인수로 넘긴 해시가 갖는 '값'이 최종 값이 된다.
아래 각각의 경우에 대한 merge 예가 있다.
다음으로 배열의 요소를 어떠한 기준으로 그룹짓고 싶을 때는 group_by 메서드를 사용하면 된다.
아래 그림을 보면 첫 번째 예는 1부터 5까지의 정수 배열을 짝수와 홀수 두 가지로 나눴고, 두 번째 예는 프로그래밍 언어에 대한 정보를 담은 해시 배열을 프로그래밍 언어가 개발된 년도를 10년 단위로 묶어서 나눴다.
group_by 메서드의 반환값은 해시가 되는데 키는 그룹핑을 위한 구분자이고 값은 해당 그룹에 속한 요소의 배열이 된다.
만약 배열의 요소를 어떤 조건을 만족시키는 것과 그렇지 않은 것 두 그룹으로 나누고 싶다면 'partition' 메서드를 사용하면 된다.
아래 그림의 첫 번째 예는 짝수와 홀수로 배열의 요소를 나눴고 두 번째 예는 1990년도 이후에 개발된 언어와 그 전에 개발된 언어로 배열의 요소를 나눴다.
partition 메서드는 반환값이 2차원 배열인데 첫 번째 요소는 블록의 결괏값이 '참'이 되는 요소들을 담은 배열이고 두 번째 요소는 블록의 결괏값이 '거짓'이 되는 요소들을 담은 배열이다.
마지막으로 몇 가지 메서드들을 더 살펴보겠다.
clear 메서드는 배열의 모든 요소를 제거하여 빈 배열로 만들어 주는데 대상 배열 자체를 변경하므로 따로 clear! 메서드가 존재하지 않는다.
compact 메서드는 배열 안에 있는 nil 을 모두 제거해 주고, shuffle 메서드는 배열 안에 있는 요소들의 순서를 랜덤하게 섞어주며, uniq 메서드는 배열 안에서 중복되는 요소들을 하나만 남기고 제거해 준다.
compact, shuffle, uniq 메서드들 모두 대상 배열 자체를 변경하는 즉, 메서드 이름 끝에 '!' 가 붙은 별도의 메서드가 따로 존재한다.
지금까지 3편에 걸쳐 루비에서 배열을 다루는 여러 가지 방법을 알아 보았는데 이것 말고도 꽤 유용하게 쓰이는 메서드들이 아직 남아 있다.
그중에서 sort, sort_by, zip 등의 메서드는 이전에 다른 글에서 다뤘기 때문에 생략을 했고 나머지는 여러분이 루비 문서 등을 통해 살펴보고 직접 테스트해 보면 좋을 것 같다.
See you again~~