루비에서 레인지(Range)는 무엇이며, 어떻게 작동할까요?
레인지는 시작 값과 끝 값을 가지는 객체로, 두 값 사이의 전체 범위에 걸친 연속적인 시퀀스를 생성할 때 사용됩니다.
1에서 20까지, 또는 "a"에서 "z"까지 표현할 수 있습니다.
이 글에서 배울 내용:
- 루비 레인지 생성 방법
step및include?메서드 활용법- 레인지의 내부 동작 원리
그럼 바로 시작해 보겠습니다!
레인지 이해하기
참고로 루비 레인지의 기본 문법은 다음과 같습니다:
(1..20)
Range를 정의할 때 괄호는 필수가 아닙니다.
하지만 레인지에 메서드를 호출하려면 괄호가 필요합니다. 괄호가 없으면 레인지 자체가 아니라 레인지의 두 번째 요소에 메서드를 호출하게 되기 때문입니다.
Range 클래스는 Enumerable 모듈을 포함하고 있어서, 배열로 변환하지 않고도 강력한 반복(iteration) 메서드들을 모두 사용할 수 있습니다.
루비 step 메서드
Range에는 step처럼 유용한 메서드들이 있습니다.
예제:
(10..20).step(2).to_a # [10, 12, 14, 16, 18, 20]
이 메서드를 사용하면 n 간격으로 레인지를 순회할 수 있습니다. 여기서 n은 step에 전달하는 인자입니다.
특정 숫자가 레인지 안에 있는지 확인하는 방법
꼭 알아두어야 할 다른 Range 메서드로는 cover?와 include?가 있습니다.
두 메서드가 같은 일을 한다고 생각하면 안 됩니다. 실제로는 서로 다르게 동작하기 때문입니다.
include? 메서드는 이름 그대로 레인지에 해당 값이 포함되어 있는지 확인합니다. 즉, Range를 Array로 펼친 후 그 안에 값이 존재하는지 검사하는 것과 동일합니다.
반면 cover?는 다릅니다. 단순히 레인지의 시작 값과 끝 값만 비교하기 때문에(begin <= obj <= end) 예상치 못한 결과가 나올 수 있습니다.
예제:
('a'..'z').include? "cc" # false
('a'..'z').cover? "cc" # true
cover? 예제는 다음 코드와 동일합니다:
"a" <= "cc" && "cc" <= "z"
true가 반환되는 이유는 문자열이 문자별로(character by character) 비교되기 때문입니다. "a"가 "c"보다 앞에 오므로, 첫 번째 "c" 뒤의 문자들은 비교 결과에 영향을 주지 않습니다.
루비에서 레인지가 구현되는 방식
레인지는 숫자와 문자에만 국한되지 않습니다. <=>와 succ 메서드만 구현되어 있다면 어떤 객체든 레인지로 사용할 수 있습니다.
예를 들어 DateTime 레인지는 다음과 같이 만들 수 있습니다:
require 'time' t1 = DateTime.new t2 = DateTime.new + 30 next_30_days = t1..t2 # Example use next_30_days.select(&:friday?).map(&:day)
그렇다면 이것은 어떻게 작동하는 걸까요? 간단한 구현 코드를 살펴보겠습니다:
def range(a, b)
# if the first element is bigger than the second
# then this isn't a sequential range
return [] if a > b
out = []
# advance until the 2nd element is the same
# as the first one
while a != b
out << a
a = a.next
end
# add last element (inclusive range)
# this also returns the results via implicit return
out << a
end
p range 1, 10
p range 'a', 'z'
이해를 돕기 위해 주석을 추가했습니다.
핵심 아이디어는 첫 번째 객체에서 next 메서드를 계속 호출하여 두 번째 객체와 같아질 때까지 진행하는 것입니다. 두 객체는 결국 만날 것이라는 가정에 기반합니다.
커스텀 클래스에서 레인지 활용하기
대부분의 경우 숫자나 문자 레인지를 사용하게 되지만, 커스텀 클래스에서 레인지를 활용하는 방법도 알아두면 큰 도움이 됩니다.
예제:
class LetterMultiplier
attr_reader :count
include Comparable
def initialize(letter, count)
@letter = letter
@count = count
end
def succ
self.class.new(@letter, @count + 1)
end
def <=>(other)
count <=> other.count
end
end
a = LetterMultiplier.new('w', 2)
b = LetterMultiplier.new('w', 8)
# Print array with all the items in the range
p Array(a..b)
여기서 핵심은 <=>와 succ 메서드를 올바르게 구현하는 것입니다.
include? 메서드를 사용하려면 Comparable 모듈을 포함(include)해야 합니다. 이 모듈은 <=> 메서드의 결과를 기반으로 하는 ==, <, > 같은 메서드들을 자동으로 추가해 줍니다.
마무리
이 글에서는 루비에서 레인지가 작동하는 방식을 살펴보았습니다. 이제 레인지를 더 깊이 이해하고, 레인지 연산을 지원하는 자신만의 객체를 직접 구현할 수 있을 것입니다.
다음 포스트를 놓치지 않으려면 아래 뉴스레터 구독도 잊지 마세요 🙂