지난번 글의 스레드 관련 예제 코드에서 Integer 클래스의 인스턴스 메서드인 times를 사용해 봤던 걸 기억할 것이다.
오늘은, 다른 프로그래밍 언어에서는 일반적으로 단순히 값 그 자체 즉, 원시 타입(primitive type)의 값으로 다루지만 루비에서는 객체로 구현된 몇 가지를 살펴보자.
제일 먼저 정수나 실수 같은 숫자들은 루비에서 단순히 그냥 값이 아니라 각각 Integer 클래스와 Float 클래스의 인스턴스 즉, 객체이다.
아래 그림을 보면 실제로 1.class의 결괏값은 Integer, 1.0.class의 결괏값은 Float 클래스인 것을 볼 수 있다.
Integer 클래스와 Float 클래스에 대해 호출한 ancestors 메서드의 결과를 보면 두 클래스 모두 Numeric클래스를 상속하고, 숫자 값들의 비교를 위해 Comparable 모듈을 인클루드하고 있는 걸 알 수 있다.
Numeric 클래스는 루비의 모든 숫자 관련 클래스의 부모 역할을 한다고 보면 된다. 즉 숫자 관련 공통 기능들을 정의해 놓은 클래스이다.
그러면 정수나 실수에 대해 호출할 수 있는 몇 가지 유용한 메서드를 알아보자.
메서드 이름만 봐도 대부분 어떤 기능을 하는 메서드인지 알 수 있을텐데, 그래도 순서대로 하나씩 간단하게 설명을 해 보겠다.
abs는 절댓값을 돌려주고, pow는 인수의 값만큼 거듭제곱을 한 값을 돌려주는데 ** 연산자 메서드를 사용해도 된다.
즉 2.pow(10) 은 2 ** 10 과 같다.
zero?는 해당 숫자가 0인지를, odd?는 홀수인지를, even?은 짝수인지를, positive?는 양수인지를, negative?는 음수인지를 알려준다.
그리고 between? 은 해당 숫자가 인수로 건넨 두 숫자 사이의 숫자인지를 알려주는데, 양 끝 숫자 중 어느 하나와 같아도 true를 돌려준다.
round는 인수 없이 호출하면 소수점 첫째 자리에서 반올림을 적용하고, 인수로 1을 주면 둘째 자리에서 반올림을 적용한다. 즉, 해당 숫자가 인수로 준 값만큼의 소수점 자리를 갖도록 반올림을 한다고 보면 된다.
ceil은 해당 숫자를 올림하여 가장 가까운 정수를 돌려주고, floor는 ceil과 반대로 해당 숫자를 내림하여 가장 가까운 정수를 돌려준다.
to_f 메서드와 to_i 메서드를 사용하여 정수를 실수로 바꾸거나 실수를 정수로 바꿀 수 있다.
이제 블록을 전달하여 반복 처리를 할 수 있는 메서드 4종 세트가 남아 있다.
그중 가장 기본적인 times 메서드는 해당 숫자만큼 그냥 블록을 반복해서 실행시켜 준다. upto 메서드는 해당 숫자부터 인수로 받은 숫자까지 1씩 증가시키면서 블록을 반복 실행해 주고, downto 메서드는 upto 메서드와 반대로
해당 숫자에서 인수로 받은 숫자까지 1씩 감소시키면서 블록을 반복 실행해 준다. 마지막으로 step 메서드는 해당 숫자부터 첫 번째 인수의 숫자까지 두번 째 인수의 값만큼 증가시키면서 블록을 반복 실행해 준다.
그리고 두 번째 인수를 생략하고 step 메서드를 호출하면 upto 메서드와 동일하게 1씩 증가시키면서 블록을 반복 실행한다.
아래 그림처럼 irb에서 4 가지 반복 처리 메서드를 간단히 테스트해 보았다.
이전 글에서 Enumerable 모듈이 자신을 인클루드하는 클래스에서 each 메서드를 제공해 주기만 하면, each 메서드를 사용해 구현한 많은 메서드들을 사용할 수 있도록 해주는 것을 보았다.
그것와 유사하게 step 메서드만 있으면 그것을 사용해 times, upto, downto 메서드를 만들 수 있다.
아래 MyInteger 클래스의 코드를 살펴보자.
우선 initialize 초기화 메서드에서는 인수로 정수를 하나 받아 @num 인스턴스 변수에 담아 놓는다.
(정수라고 가정하고 로직에만 집중하자!)
step 메서드는 전통적인 while 반복문을 사용해서 변수 i를 @num의 값부터 to 파라미터의 값까지 inc 파라미터의 값만큼 증가시키면서 블록을 실행시킨다.
Integer 클래스의 times 인스턴스 메서드는 객체 자신이 나타내는 숫자만큼 블록을 반복 실행해 주는데, 블록 파라미터에 0부터 넘겨 준다. 이점을 생각하면서 times 메서드의 코드를 봐보자.
class MyInteger
def initialize(num)
@num = num
end
def step(to, inc = 1)
i = @num
while i <= to
yield(i)
i += inc
end
end
def times(&block)
MyInteger.new(0).step(@num - 1, &block)
end
def upto(to, &block)
step(to, &block)
end
def downto(to)
diff = @num - to
i = 0
step(@num + diff) do |n|
yield(n - 2 * i)
i += 1
end
end
end
times 메서드를 보면 0을 인수로 하여 MyInteger 객체를 새로 생성하고, 이 객체에 대해 step 메서드를 호출하고 있다.
이때 0부터 시작이므로 step 메서드의 첫 번째 인수에는 @num이 아니라 @num - 1 을 넘겨줘야 맞다.
그다음 upto 메서드는 step 메서드의 두 번째 인수의 기본값이 1이므로, 단순히 upto 메서드가 받은 인수를 step 메서드의 첫 번째 인수로 주고, 두 번째 인수는 생략한 채로 호출해 주면 된다.
마지막 downto 메서드는 조금 생각할 거리가 있는데, 왜냐하면 downto는 현재 숫자 값보다 1씩 작아지면서 반복 실행이 되어야 하지만, step 메서드에서 while 반복문의 조건식(i <= to)을 보면 변수 i의 값이 양의 방향으로 커져야지만 결국 반복문이 종료가 되는 구조이다.
따라서, 현재 MyInteger 객체가 갖는 값(@num)부터 시작해서 @num와 to의 차이(diff)만큼 더 큰 값까지 1씩 증가시키면서 반복되도록 step 메서드를 호출하되, 블록 파라미터에는 @num부터 시작해서 1씩 작아지는 값을 전달해야 한다.
결국, downto 메서드의 블록에는 step 메서드의 블록으로 전달된 값에서 2의 배수만큼 뺀 값을 전달해 줘야 한다.
예를 들어, 5부터 1까지로 downto 메서드를 호출한다고 하면, 내부에서 5부터 9까지로 step 메서드가 호출이 되면서 step 블록에 5, 6, 7, 8, 9 가 전달이 되는데, 결국 이 값이 downto 블록에는 5, 4, 3, 2, 1 로 전달되어야 한다.
두 값을 비교하면 처음에는 5로 같지만 그 다음부터는 2, 4, 6, 8 만큼 차이가 벌어지게 된다.
테스트를 위해 MyInteger 클래스의 코드를 D:/blog/ruby/everything 폴더에 my_integer.rb 파일로 저장하고, 아래 테스트 코드 역시 같은 폴더의 test_my_integer.rb 파일에 저장하자.
require './my_integer'
my_3 = MyInteger.new(3)
my_3.times { |n| puts "my_3.times: #{n}" }
my_3.downto(1) { |n| puts "my_3.downto(1): #{n}" }
my_1 = MyInteger.new(1)
my_1.upto(3) { |n| puts "my_1.upto(3): #{n}" }
my_1.step(5, 2) { |n| puts "my_1.step(5, 2): #{n}" }
my_1.step(5) { |n| puts "my_1.step(5): #{n}" }
test_my_integer.rb 파일을 실행해 보면 times, upto, downto, step 메서드 모두 잘 동작하는 걸 볼 수 있다.
다른 프로그래밍 언어에서는 원시 타입의 값으로 다루지만 루비에서는 객체인 또다른 예로는 nil, true, false가 있다.
아래 그림을 보면 nil, true, false 각각 NilClass, TrueClass, FalseClass의 객체인 것을 알 수 있다.
nil, true, false는 각 클래스의 유일한 객체이고, 따라서 세 클래스 모두 new 메서드를 사용하여 새로운 객체를 생성할 수 없다.
'값이 없음'이나 '초기화되지 않음'을 의미하는 nil과 '참'을 의미하는 true 그리고 '거짓'을 의미하는 false가 여러 개일 필요는 당연히 없어보인다.
같은 이유로 Integer 클래스나 Float 클래스 역시 new를 통해 임의적으로 객체를 생성할 수 없다.
이렇게 숫자나 nil 그리고 불린 값까지도 객체로 표현한 것에는 루비 언어의 설계 철학이 반영되어 있다고 보면 될 것 같다.
모든 것을 객체로 다룰 수 있으므로, 특히 null(다른 언어에서는 nil이 아니라 보통 null로 부른다)검사를 위한 별도의 코드 없이 일관되게 코드를 작성할 수도 있다.
예를 들어, 루비 배열의 join 메서드는 다음 그림처럼 모든 요소의 문자열 표현을 연결한 새 문자열을 돌려주는데,
이러한 join 메서드를 만든다고 했을 때 아래 코드처럼 nil이나 숫자 등에 대해 별다른 처리 없이 그냥 일관되게 to_s 메서드를 호출하여 받은 문자열을 구분자(del)와 함께 << 메서드로 이어 붙여 나가기만 하면 된다.
require './my_enumerable'
class MyArray
include MyEnumerable
def each
yield("I")
yield("love")
yield("you")
yield(1)
yield(nil)
yield(2)
end
def join(del = "")
reduce do |a, b|
a.to_s << del << b.to_s
end
end
end
my_arr = MyArray.new
p my_arr.join
p my_arr.join(", ")
위의 코드를 MyEnumerable 클래스의 소스 파일이 있는 폴더에 test_join.rb 파일로 저장하고 실행해 보자.
irb 에서 배열로 join 메서드를 테스트했던 것과 동일하게 동작하는 것을 볼 수 있다.
참고로, 아래 MyEnumerable 클래스의 each_with_index 메서드와 reduce 메서드의 코드를 옮겨 왔다.
MyEnumerable 클래스의 전체 코드를 보길 원하면 'Enumerable 모듈 파해치기4' 글을 참고하길 바란다.
module MyEnumerable
def each_with_index
i = 0
each do |e|
yield(e, i)
i += 1
end
end
def reduce(accu_val = nil)
each_with_index do |e, i|
if i == 0 && accu_val.nil?
accu_val = e
else
accu_val = yield(accu_val, e)
end
end
accu_val
end
...생략
end
See you again~~