· KLDP.org · KLDP.net · KLDP Wiki · KLDP BBS ·
Humble Little Ruby Book/Chap3


1. 유도와 흐름(제어)


흐름제어는 대부분의 어플리케이션에서 중요한 부분을 차지합니다. (작성한 프로그램이 항상 동작의 한 방향을 취하지 않는한 이 장의 대부분의 코드를 복사해서 붙여넣으려는 식의 꽁수부리려는 수작은 삼가하기 바랍니다). 흐름제어는 상태에 대한 행동을 취하기 위한 행동규범(?)을 수립하도록 도와줍니다.(i.e. 각기 다른 코드를 경우에 따라 수행하려 하는경우...), 같은 갈래상의 코드를 여러번 실행할 수 있거나 어떤 상태가 만족스럽지 못한경우에 우회할 수 도 있습니다. 두가지 "일반적인" 흐름 제어 구성이 있는데 상태와 반복입니다. 이것들을 끝내지 않으면 똥통에 빠져 몹시 당황한 미친X인 마냥 여기에서 포기한 꼴이 되므로 필자는 이장을 통해 각각의 요소를 제대로 정리하여 안내할 것입니다. 정신 바짝차리고 따라오시기 바랍니다. -_ㅡ)+


1.1. 상태

상태 구조는 각각의 경우에 대한 선택의 결과를 만들어냅니다. 프로그램 내부에서 상태를 기반으로 하는 각각의 코드를 실행할 수 있도록 갈래를 형성하게끔 합니다. 값을 비교하고 참 거짓 여부를 판단하여 해당 갈래에 대한 코드를 실행하게끔 합니다. 이와 같은 요소는 루비 프로그래밍 세계의 교통 신호등과 비슷한 역할을 한다 생각하시면 되겠습니다.

1.1.1. if 문

루비에 있어 매우 기초적인 상태문은 if문입니다. 이는 값들을 간단히 비교하고 참 거짓을 판단합니다. 만약 참이라면, 주 코드가 실행되고, 거짓이라면 else 항의 코드를 실행하지만, else 항 마저 없는 경우에는, 그 어떤 if 블록 내의 코드를 실행하지 않고 그대로 넘어갑니다..


if.jpg
[JPG image (56.11 KB)]

Figure 7: 만약 내가 바다에 사는 고기라면, 기쁨에 넘쳐 헤엄치리오...


모든 if문의 결과는 다양한 상태 관계에 대한 참이나 거짓입니다. 다음의 예시를 보도록 합시다:

if not (true != false) && (true != true):
    puts "Whoa! Alternate dimension!"
elsif (false === 'false') then
    puts "Type conversion? I think not..."
elsif (true == 1) or (3 == false)
    puts "Booleans != Numbers"
else
    puts "I guess we're still OK!"
end

→ I guess we're still OK!

여기에서 참 많은 것을 볼 수 있습니다. 그렇기 때문에 이 소스코드의 부분부분을 시시때때로 쪼개어 실행해보도록 합니다. if 문은 동등여부 등에 따른 상태비교값에 의한 키워드에 의해 구성됩니다. if 문은 자체적으로 상태 블록을 엽니다 (i.e. if는 begin/end블록의 시작점입니다). if키워드로 시작하고 end 키워드로 끝을 맺는 단위 코드를 활용하여 if문 블록을 간단히 만들 수 있습니다. 이 예제를 통해 if 문을 만들기 위한 가능한 구성을 모두 제시하였습니다. elsif 와 else 키워드는 if 문에서 참 조건을 충족시키지 못할 경우 다른 경우에 관한 갈래로 이동할 수 있도록 합니다. elsif 는 다른 상태 비교 시험을 통해 참 조건을 만족시키는 경우 해당 코드를 실행할 수 있도록 합니다. 그러나 else는 if와 elsif의 그 어떤 조건도 만족시키지 못했을 경우에 코드를 실행할 수 있도록 길을 열어줍니다. 이 얘기는 결국 수많은 elsif를 포함하고 있는 if 블록 하나를 만드는 것이 낫지, 굳이 if 블록을 10개정도 더 만들 필요는 없다는 얘기가 됩니다. (with the safety net of the else statement to boot).

두번째로 우리가 보아야 할 것은 상태문에서 사용할 연산자들입니다. 동등성을 비교하기 위해 "==" 연산자를 사용합니다. (두 개의 등호 기호를 표기하지만 그것 뿐만이 아닙니다. 만약 한개를 사용했다면 결과가 참으로 나오게 되는데 이는 등호 한개를 사용함으로 인해 값을 변수에 할당하였으므로 변수가 0이 아닌 어떤 값 자체를 포함하여 참이 되기 때문입니다). 만약 같지 않음 상태를 비교하려면 != 연산자를 씁니다. 이 엄청난 양의 문단을 통해 이를 설명하느니 차라리 아래의 표를 이용해 간단하게 Ruby에서 사용되는 연산자를 알아보는 편이 속시원할지도 모르겠습니다:

Ruby에서의 상태 연산자
a == b a 와 b가 값이 같을때 참.
a === b a와 b는 값이 같으면서 자료형도 같을때 참.
a != b a와 b는 같지 않을때 참.
a <> b
a !== b a와 b가 값이 다르면서 자료형도 다를 경우 참
a < b a의 값이 b보다 작으면 참.
a > b a의 값이 b보다 크면 참.
a <= b a가 b보다 작거나 같으면 참.
a >= b a가 b보다 크거나 같으면 참.
세번째로 주목할 사항은 논리 연산자입니다. 이것들은 상태를 연쇄적으로 비교하기 위해 사용하는 특별한 연산자로서 이를 통해 특별한 상태의 값을 구합니다. Ruby는 두가지 방식의 연산자를 지원합니다: C-style 과 문자열 방식입니다. C-style 연산자는 처음 예제에서와 같습니다. && 는 "그리고(and)", || 는 "또는(or)" 과 같은 식입니다. 문자열 방식 연산자는 상징적으로 동일하게 간주됩니다.: and, or, 그외의 것들.... 이 표를 통해 그 두가지 형태와 쓰임새에 대해 정리했습니다.

Ruby 에서의 상태 연결 연산자
&& and 양 조건이 다 참일때.
</td><td > or 양 조건중 한쪽이 참일때.
! not 이 연산자를 붙임으로 인해 참은 거짓이 되고 거짓은 참이 됨 (i.e. it works like !=).


1.1.2. 3중 연산자


여기서 이제 if 문에 관한 고전적인 방법을 배우게 됩니다. 필자는 이 간단한 예시 일부를 통해 보여주고 싶습니다. 이 3중 연산자라는놈은 (잡담: 3중 연산자는 3부분으로 나뉘어졌기 때문에 이렇듯 간단하게 이름이 붙었습니다) 굳이 if - else 블록을 만들지 않아도 실행할 수 있도록 설계되었습니다. 다음의 예를 통해 if 문을 3중 연산자로 바꾸는 방법을 보여드리도록 하겠습니다:

if ('Yes' == 'No') then
   puts "Wrong!"
else
   puts "Yeah, baby!"
end
→ Yeah baby!

('Yes' == 'No') ? (puts "Wrong!") : puts ("Yeah, baby!")
→ Yeah baby!

3중 연산자는 물음표 다음에 콜론으로 구분되는 상태문으로 구성되어 있습니다 (등호 연산자는 if 문에서 쓰는 방법이랑 같습니다). 괄호는 콜론으로 구분되는 양 자의 경우에는 별로 중요한 것이 못됩니다만, 가독성을 향상시키고 각각의 갈래를 다중라인으로 설계할 수 있도록 합니다. (i.e. 여는 괄호를 시작 코드줄의 앞에 놓고 닫는 괄호를 끝나는 코드줄의 마지막에 넣습니다). 이러한 다중라인 행동지시는 별로 추천하고픈 방법이 아닙니다. 왜냐면 이러한 방법을 사용함으로 인해 약간 코드를 못 알아먹게끔하며, if 문의 비교 결과에 대한 타이핑으로부터 해방시켜주지 않습니다.

1.1.3. Unless

루비에서 unless 상태문은 "반전된" 문입니다. 왜 "반전된"이란 표현을 썼냐 하면 if에서는 참일경우 주 코드를 실행하지만 unless 문에서는 else문 아래의 코드를 실행하기 때문입니다. 예를 들자면:

unless ("I am true." == "I am true."):
    puts "Something wonky!"
else
    puts "All is alright!"
end
→ All is alright!

"I am true." 는 "I am true."와 같으므로 조건은 참이 됩니다. 주 코드는 실행이 안되지만 else 갈래 이하 구문은 실행됩니다. 이 구조는 코드상에서 if문의 가독성 향상을 제외하고는 더이상 제공되는 것이 없습니다. (e.g. 네거티브한 if문을 수도없이 갈겨쓰는 것을 막기 위해서, 이는 "이게 이것이 아니라면 이것을 해라"라고 하느니 차라리 "이게 아닌한 이것을 해라"라고 말하는게 더 쉽고 편할 것입니다. 긍정적인 [http] chi를 유지하는 것은 루비의 감성적 건강에 있어 매우 중요합니다).

(괄호 안을 번역하고 보니 헷갈리는군요... 데체 무슨 말인고 -_-;;)
if 문을 작성할 때 조건문을 긍정형으로 쓰라는 말인 것 같네요. 부정형이 들어갈 경우에는 차라리 unless를 쓰라는 것이구요. 저도 if문에 걸린 부정형 조건 때문에 헷갈린 적이 많네요 ^^ 그리고 chi가 문맥상 Computer Human Interaction을 뜻하는 것 같아 링크 걸었습니다. --아무개

1.1.4. 상태문 변형

if문은 또한 변형자라는 매력적인 문법적 조미료를 제공합니다. 이를 생성하는 것으로 하여금 if 문이 조건문 마지막에 표기할 수 있도록 허용하고, if문이 실행되든지 아니든지간에 일단 조건부터 판단하도록 합니다. 다음을 보시면.... :

thelma_louise = 13
puts "She's less than 15 alright!" if thelma_louise < 15
puts "She ain't more than 12, though!" if thelma_louise < 12
→ She's less than 15 alright!

보시는 바와 같이 줄 마지막에 if문을 위치시켰을때도 코드를 실행하기 전에 먼저 뒤에 제시된 조건 판단문을 통해 값을 비교한 다음에 이것이 참인 경우 해당 if문 앞에 위치한 실행문을 실행시킵니다. 위와 같은 코드는 아래와 같이 정리할 수 있습니다.:

if thelma_louise < 15:
   puts "She's less than 15 alright!"
end

이렇게 하는 것이 보다 간결하고 우아한 해결 방법입니다. 이것이 진정 Ruby다운 것이지요. 이 코드는 다음과 같이 begin/end 블록을 표시할 수도 있습니다:

begin
   puts "It's so true!"
end if (true == true)
→ It's so true!

이러한 방법은 500줄에 달하는 if 문으로 이루어진 거대한 코드의 일부를 제어하기 위해서 제시된 매우 우아한 해결책 입니다. (특히 end 키워드를 통해 어느 시점에서 if문을 끊어야 하는지 명료화 됩니다.).

1.1.5. case 구문

만약 이걸 알아채지 못했다면, 타수가 적은 깔끔한 코드의 모든 것이 여기있다고 보겠습니다 (누가 이게 아니래드라..?). 대부분의 변변치 못한 코드에 같은 수를 자꾸자꾸 집어넣는 if와 elsif 따위보다는 이것이 점점 사람성격을 급하게 만들고 나중에는 코딩속도를 향상시킵니다. 구문적으로 코드상에 삽입하기 아주 매력적인 이것을 case 구문이라 합니다. case 구문은 루비에서 두 형태로 존재합니다. 첫번째 유형은 "표준" 유형입니다: "target" 변수를 놓고 이 변수에 대해 다양한 값을 놓고 비교를 하게 됩니다.:

the_tao = 1234

case the_tao
    when 666: puts "That's such a lie!"
    when 1337
        puts "Liarrrr!"
    when 32767 then
        puts "Whatevurrrr!"
    else
        puts "You are harmonized with the Tao."
end
→ You are harmonized with the Tao.

이러한 case 구문의 유형은 각각의 경우들에 제시된 다양한 값에 대하여 간단하게 비교해봅니다. case구문은 when 이라는 키워드 다음에 문자열이 오고 그 다음 콜론이 따라오는 형태로 이루어져 있습니다. (어떤 유형이 어떤 줄의 다음에 시작되느냐 하는 이런 따위의 것은 중요한 것이 아닙니다.); when 키워드는 각각의 경우를 분리하며, "end" 키워드는 case구문을 종료하기 위해 필요합니다. 각각의 경우에 대해 위에서부터 쭉 검사합니다. 만약 일치점이 발견되면 해당 가지를 따라 실행되고 end로 따라가서 바로 블록을 빠져나오게 됩니다. 만약 어떤 조건도 일치하는 것이 없다면 else 항목을 실행합니다. (if 블록과 좀 비슷하다고 보시면 되겠습니다).

다음에 나오는 두번째 case 구문의 유형은 if 문과 비슷하게 상태문을 만들고 case에 명시되는 비교대상을 없애서 유연성을 가미하였습니다. 심지어는 비교 대상이 없어진다 하더라도 여전히 처음 나온 유형처럼 case문을 굴려먹을 수 있습니다.

enlightenment = 42

case
    when enlightenment > 60:
        puts "You are too hasty, grasshopper."
    when enlightenment < 40 or enlightenment == nil:
        puts "You are like the sloth, my friend. Diligence is key!"
    when enlightenment == 42:
        puts "Hello, Enlightened One."
    else
        puts "Yeah, not quite, pal. Maybe next time."
end
→ Hello, Enlightened One.

보시는 바와 같이 이건 첫번째 유형에서처럼 한가지 변수를 대상으로 비교연산을 하지만 첫번째 유형 보다 강력한 비교 연산을 지원합니다. 상단의 case에서 시작할때 첫번째 비교연산에서 일단 오른쪽으로 구문을 실행하고 내려갑니다. 무슨 얘기냐...면 경우가 일치할때 (i.e. 경우가 참이되기 위해 필요한 모든 조건은 줄줄이 엮인 상태문과 만납니다.) 가지에 딸린 구문을 실행하고 case구문을 빠져나옵니다.

두 유형에 있어 공통점이 있다면 "폭포수 유형"의 실행 구조를 가지고 있다는 것입니다. 대부분의 프로그래밍 언어에서도 아시겠지만 case 블록은 어떤 키워드 같은게 필요합니다. ( 종종 break 같은 것들이 그렇지요 :) ) 이건 결국 무슨 얘기냐면 어플리케이션이 이 키워드를 만나기 전까지 조건을 검사하고 블록이 끝나는 지점까지 실행하다가 break같은걸 만나면 아얘 case를 빠져 나오고 break를 안 만나면 그 다음 case안의 구문을 실행합니다. 예를 들어 다음의 C언어 코드의 일부를 보도록 하겠습니다.:

int my_number = 42;

switch(my_number) {

    case 41:
        printf("A little higher...");
    break;

    case 42:
        printf("You have found the answer!\n");

    default:
        printf("And have fallen into a hole in the C compiler!\n");
    break;

}


→ You have found the answer! → And have fallen into a hole in the C compiler!

If it's not evident from the code, the break keyword will exit the switch block if it is encountered. This keyword is not needed in Ruby since it doesn't have fall through, but since C-style languages do, the default block (like Ruby's else in case statements) gets executed because there was no break in the block that evaluated to true (i.e. case 42). As helpful as the fall through mechanism is, it can really be quite annoying when the lack of one break statement causes your entire application to come to a screeching halt.

1.2. 반복구문

반복구문은 타수를 줄여주고 같은 혹은 유사한 다른 코드가 여러번 반복되어 실행되는 대부분의 작업을 도와주며 필요한 동작에 따른 상태에 대해 응답해줍니다. 반복구문은 여러번에 걸쳐 입력되는 내용에 대해 어떤 조건을 만나기 전까지 혹은 각각의 수집물에 대한 요소를 처리하는 구문의 반복에 있어 핵심적입니다. 이 섹션을 통해 우리는 이들 유형을 다 까볼 것입니다.

1.2.1. 조건 반복

조건 반복은 제시된 조건문을 바탕으로 실행합니다 (if나 unless 블록과 같은 곳에서 같은 조건문들이 사용된 경우). 이러한 반복 유형은 두가지로 존재합니다: while 과 until이 그것이죠. while 반복은 단지 while 조건이 참일 경우에만 실행할 것입니다.:

x = 0
while(x < 10):
print x.to_s # print is like puts without a \n at the end
x += 1
end
→ 0123456789

각각의 반복동작 수행시, 조건을 항상 검사하며 이것의 결과가 참인 경우에는 블록 안의 구문을 실행합니다. 그게 아니라면 블록을 빠져나가고 그 다음 구문을 실행합니다.

while.jpg
[JPG image (14.83 KB)]

Figure 8: The while loop: 1973년부터 시작된 표준.


돌려 말하자면, while문은 반복구문 꼭대기에 if 구문을 놓고 if 구문이 거짓이 될때까지 주 코드를 실행하는 것과 마찬가집니다. 그러나 until 반복구문은 if 대신 unless를 위치한 것과 같은 결과 양상을 보입니다:

x = 0
until(x >= 10)
print x.to_s
x += 1
end
→ 0123456789

각각의 반복 과정에서 조건은 단지 while 반복구문과 비슷하게 검사되지만, until 구문에서는 조건이 만족하지 않는 동안, 즉 if 구문이 거짓이 되는 동안 실행됩니다.


until.jpg
[JPG image (55.47 KB)]

Figure 9: until구문이 while구문보다 관심이 덜 해서 while을 시샘하는거 같은데...?


unless조건과 거의 같게, until 반복구문에서는 조건이 같으면 블록 구문을 넘어갑니다.

이 반복구문들은 반복적 수행에 매우 유용합니다(같은 작업을 열번 반복한다거나, 순서대로 데이터를 출력한다거나, 예제에서와같이 계속 할 경우)만, 함정이 숨어 있습니다. 처음 구문을 실행하기전 조건이 블럭안의 구문을 수행하기 위한 상태를 만나지 못한다면(while을 위한 참조건이나 until을 위한 거짓조건 같은것...) 반복구문은 절대로 실행되지 않습니다. 값을 제어하는 반복구문 밖에 또다른 반복구문이 그 값을 제어하는 경우의 가능성을 인지하는 것은 루프를 실행하고 난 다음의 응용프로그램 실행에 있어 중요합니다. 이 조건은 어떤 문제를 야기할 수도 있고 그렇게 할 것입니다. 그래서 변수에 대해 진리(참거짓)를 반드시 미리미리 확인해야 합니다.

어떻게든 그럭저럭 봐줄만한 더욱 또라이같은 머리를 치켜들만한 또 다른 함정은 조건문이 멈추지 않을 수 있다는 것입니다. (while 반복문이 거짓소건에 도달하지 않는다면...?) 그래서 무한 반복에 빠져들게 하고 이것을 혼란스러움의 창조로 유도하게 됩니다. 결국 실제 이러한 일이 일어난다면 만물을 축약할 수도 있지만 전 이걸 검증하지 못합니다. 그래서 이걸 어떻게 하면 이러한 사태로부터 예방할 수 있을까요? 초보자들에게는, 가능한한 조건문을 유연하게 사용하는지 확인하라고 하고 싶습니다. (예를 들자면 >= 연산자가 == 연산자보다 나을 수 있는데 이것은 수를 점점 크게 세어나갈때 고의적으로 계획했던 수 이상을 넘기는 것을 막기 위함입니다. 이건 모든 상황에서 최선의 방책이 될순 없지만, 충분히 고려할만합니다.) 그리고 상태값을 통해 코드로 상호작용 동작을 취하여 확인하시기 바랍니다(상태에 영향을 주기 위한 그 어떤 행위에 대해 확인한다면 아마 대부분의 사람들이 이걸 까맣게 잊고 있다는 사실에 무척 놀라실겁니다).

만약 초고수 über‐1337 프로그래머였다면 자신에게 이렇게 말했을지도 모르겠습니다. "Yo, 또라이! 난 그딴거에 걱정하지 않아 왜냐면 나는 그... 지랄 방정맞을 for 루프(fo' loops fo' rizzle)... 를 쓰거든~~!" 아마 맞는 말일지도 모르고 실제로도 루비를 다른 언어에서 수용하도록 내버려 두었을 것입니다. 네!! 루비는 비록 전통적 개념의 "for loop" 를 제공하는건 아니지만, 어쨌든 for 키워드 사용을 제공합니다.

(원작자의 말투가 좀 거칩니다... 번역상 이해를 -_-)

1.2.2. 루프와 블록의 반복

개발자로서의 내 인생에 있어, 말도 안되는 코드의 일부를 꽤 많이 보아왔습니다. 텍스트 파일로부터 오는 모든 것을 사람들에게 그들의 응용프로그램 개발 과정에 있는 동안 "적어도 17번 바퀴를 다시 발명하도록 하는 것과 같이" 엄청난 데이터양이 흐르는 데이터베이스와 같이 사용하도록 한다는 것입니다. (정리하자면, 텍스트 파일을 다룬다는 것 자체가 그리 효율적인 것이 아니라는 것입니다. 역자 주.) (모든 개발자, 프로그래머, 해커들로부터 나온 얘깁니다: 대부분의 언어가 날짜/시간 제어루틴을 가지고 있는데, 이 얘기는 결국 and/or 모듈을 사용할 필요가 없다는 것입니다. 참 고맙네요). 이 사람들은 단지 정직하게 그들 자신과 그외 다른 사람들을 위해 더 어렵게...고생해서 만든 것입니다. 이러한 방법의 종류로 하여금 저는 단지 언어의 환경을 좀더 낫게 하기 위해 언어상의 상태루프를 "훔쳐"쓰는 사람들을 볼때 제가 느끼는 바입니다. 예제를 한번 보도록 하지요

my_array = [13, 1, 4, 5, 29]

i = 0

while (i < my_array.length)
    print my_array[i].to_s + " "
    i += 1
end

→ 13 1 4 5 29

동작이야 하겠지만 이건 최상의 정답이 아닙니다. (특히 루비에선 더욱 그렇습니다). 루비는 이러한 집합요소를 안전하고, 쉽고 능률적으로 반복하는 짝짝이의 방법을 제공합니다. _-_); 이것이 루프 반복입니다. 이것들에 있어서 첫번째는 for 루프입니다. 루비에서의 for 루프는 다음에 정리된 바와 같이 C나 C로부터 파생된 언어계열에서의 for루프와는 비슷하지 않습니다. 그러나 이러한 점으로 인해 저는 이것이 무한한 유용성을 지니고 있다고 봅니다.

루비에서의 for 루프는 Python에서의 for루프와 매우 (거의 이상적으로) 유사하다고 봅니다. 콜렉션(집합?) 이라는 것이 제공되고 for루프로 하여금 변덕스러운 각각의 아이템을 반복적으로 잡아냅니다.

for.jpg
[JPG image (25.28 KB)]

Figure 10: 어떤 집합적 요소가 사용될때 반복자는 상태루프보다 쉽고 안전하게 다룹니다.


다음 코드 예제를 참고해봅시다! 우리는 이제 짝수 배열을 만들고 반복자를 사용하여 이를 출력할 것입니다.

my_evens = [2,4,6,8,10,12,14]

for my_int in my_evens
   print my_int.to_s + ", "
end
→ 2, 4, 6, 8, 10, 12, 14,

이 얘기는 결국 "my_evens에 있는 각각의 객체를 위해 my_intfmf I want to create a local variable my_int for each object in my_evens and do something with it." Every time the loop iterates, the next value in my_evens is copied into the variable my_int; this variable allows you to manipulate the item in the collection easily (and, since the original object is copied into that variable, you can manipulate it without risk of bit twiddling in the original collection).

The for loop is really just salacious syntactic sugar for an iterator; an iterator is simply a block of code that iterates over a collection provided by the each method of a class. For example, let's look at the above example in the iterator form:

my_evens = [2,4,6,8,10,12,14]

my_evens.each do |my_int|
print my_int.to_s + ", "
end
→ 2, 4, 6, 8, 10, 12, 14,

An iterator is formed by creating a begin/end block (discussed above) but using do/end as the initiator; you are essentially creating a special type of begin/end block that will be iterated for each item in the collection. This type of loop naturally guards against the variable discord that you may encounter by incrementing an index using something like a while loop (i.e. you go over the value you want if using the == operator, your value never gets above the needed value if using a < operator, etc.) while also providing a more natural way to access the value you wanted. Unfortunately this convenience comes at the price of flexibility. In a while or until loop you can alter how quickly your conditional reaches the state that allows the loop to break (also called step) by adding more than 1 to the value you're keeping track or something like that; this ability is severely limited in an iterator loop (i.e. it doesn't exist unless you want to use the next method somehow to make it happen). Keep this in mind when you decide which type of loop to use; it can greatly affect the performance and stability of your application.

1.2.3. Statement Modifiers

Loops offer a construct very similar to the if statement's modifier. Loops are great ways to save time and code, but they aren't very "natural"; I mean when repeating a task (such as banging your head into a mirror out of sheer frustration) you typically don't think in the form of a loop construct.

while(self.conscious == true)
   self.head_bang("mirror")
end

That's simply not how our mind usually works, and fortunately, Ruby, in all of its readable glory, has decided to bless us with the loop statement modifier construct. For example:

self.head_bang while self.conscious == true

This construct is not only more natural to read and say, but it is also more concise; you still have all the same functionality but in much less code and hassle. This construct works with both while and until loops (but not any of the iterating loops); it also works with begin/end blocks which allows you to do post-test loops (i.e. loops that always execute at least once before checking the conditional as opposed to the pre-test loops that we've been using). For example, if you wanted to output the English translation of "Tora! Tora! Tora!" you could write:

counter = 0

begin
   print translate("Tora! ")
   counter += 1
end until counter == 3
→ Briefcase! Briefcase! Briefcase!

In this setup, the block will always execute at least once regardless of the truth of the attached conditional (unlike if you simply use the while or until loop). Do keep in mind when using this construct that you are still, at its core, using the while and until loop, which means that it is still susceptible to the same pitfalls and problems with variables. Make sure to test your application thoroughly for any potential problems related to this.

1.2.4. Controlling Loops

Does it seem that loops have your application in a strangle hold? They do seem to be hulking, domineering, unstoppable behemoths that stop only when they darn well feel like it. Well, actually they probably just seem like really useful constructs that are hard to control without artificial bit twiddling in the conditional. For example:

my_x = 115
my_y = 40
temp = 0

while(my_x < 150)
    if (my_x % my_y) == 0: # if the quotient is even
        temp = my_x
        my_x = 151
    else
        my_x += 1
    end
    puts my_x
end

my_x = temp
puts my_x
→ 120

That's a bit dangerous if you ask me (and if you're reading this, you just might); artificially altering the conditional value can lead to some craziness in your application (i.e. accidentally skipped code, the variable being used outside of the loop without the temp value being stored back into it, etc.). In Ruby, there are various keywords that allow you control the flow of loops. The next keyword allows you to skip the current iteration, while the break keyword allows you to exit the current loop completely. Let's look at our previous example again:

my_x = 115
my_y = 40

while(my_x < 150)
    my_x += 1
    puts my_x
    if (my_x % my_y) == 0: # if the quotient is even
        break
    else
        next
    end
end

puts my_x
→ 120

The usage of the next keyword in this example is rather inordinate (i.e. I should have simply let the loop iterate again rather than forcing it), but I wanted to demonstrate how the next keyword works. This loop works just as before, except more concise and less bloated.



Figure 11: Break. I always get a great degree of satisfaction from breaking a loop. Take that sucker!


The break keyword breaks the loop and continues on to the code after the loop just as if the conditional had been met. The next keyword skips past all remaining code (which in this example wasn't much) to the end of the loop and continues on to the next iteration. But, wait! It gets fancier:

my_x = 115
my_y = 40

while(my_x < 150)
    my_x += 1
    puts my_x
    break if (my_x % my_y) == 0 
    next
end

puts my_x
→ 120

You can use a conditional modifier with the break or next keywords! And, yes, the next keyword isn't necessary, but I wanted to demonstrate two things. Firstly, the keyword's usage (I know you probably understand it by now, but another example never hurt anybody!). Secondly, all code after break is skipped. The loop does not iterate again because the conditional attached to break was satisfied.

1.3. 예외

자, 그래서 이제 프로그램을 마무리 하려는데, 갑자기 이게 미친 에러가 벌떡벌떡거리드니만... 이게 당신이 어떻게 해야 할지 모르게 가로막고 있다 칩시다. 음.. 다른 친구들보다 여드름이 엄청나서 사회적으로 왕따를 당해버린 -_- 짖궂은 13살짜리 꼬마 애들이 "YO d00D I"M 1337!!11"와 같은 방식으로 숫자로만 이루어진 프로그램에 치명적 파괴를 입힐 문자열?을 입력하길 원한다 합시다. 프로그램이 깨지게 그냥 내버려 두시겠습니까? 영원한 재와 연기로 남길 바랍니까? 아마 별로 환영하시지 않을 겁니다. 왜냐면 시골뜨기 마을로부터 온 "TO01337ANdY"... IA? 두려워 하지 마세요. 어린 초심자 입니다! 루비는 이미 당신이 발견한 것을 지녔습니다... 그게 뭐냐면... 그게 뭐냐면...

에러와 이와 관련된 다른 문제를 프로그램 내에서 전형적인 방법을 통해 다루기 위해 예외라는 간단한 방법을 제공합니다. 모든 예외는 Exception이라는 기반 클래스로부터 상속됩니다. 루비는 당신이 사용하도록 (또는 어플리케이션상의 문제를 제어하도록) 수많은 내장 예외 객체를 제공합니다. 현재 사용가능한 내장 예외 클래스에 관한 표가 아래에 있습니다.

1.3.1. Handling Exceptions

예외가 발생했을때 이를 제어하기 위해서는 복구 블록을 생성해야 합니다. 제시된 예제를 잘 봐주시고, 배고픈 독수리처럼 이를 확 자기의 것으로 낚아채시기 바랍니다. (아니면 루비에서 어떻게 하는지 정말로 알고 싶은 사람처럼요):

def method_of_doom
    my_string = "I sense impending doom."
    my_string.ah_ha_i_called_a_nonexistent_method
end

method_of_doom
→ ! NoMethodError

이제 예외 속성을 제어해봅니다.:

def method_of_doom
   my_string = "I sense impending doom."
   my_string.ah_ha_i_called_a_nonexistent_method
rescue Exception:
   puts "Uhh...there's a problem with that there method."
end

method_of_doom
→ Uhh...there's a problem with that there method.

복구 블록을 생성하기 위해 단지 Exception 클래스 앞에 rescue라는 키워드를 추가하기만 하면 됩니다. (루비는 수많은 내장 예외를 제공합니다. 전체 목록을 참고하시고 싶으시다면 루비 문서를 확인해보세요). 제가 왜 Exception을 썼냐면 이것이 일부 혹은 모든 예외를 잡지만, 이 제시된 하나만으로도 다른 방식으로 다른 형태의 예외를 제어하기 위해 수많은 복구 블록을 정의할 수 있고, 혹은 단일 복구 블록을 통해 다양한 예외 유형을 제어할 수 있습니다.

def method_of_doom
   my_string = "I sense impending doom."
   my_string.ah_ha_i_called_a_nonexistent_method
rescue NoMethodError:
   puts "You're missing that method, fool!"
rescue Exception:
   puts "Uhh...there's a problem with that there method."
end

method_of_doom
→ You're missing that method, fool!

다형성 복구 블록을 정의했다면 아마 루비는 발생한 예외와 같은 형태의 예외를 제어할 수 있는 처음 부분의 예외 복구 블록을 따라갈 것입니다. 이 예제에서 Exception 블록이 NoMethodError위에 위치하고 있었다면 처음의 예제와 같은 결과를 볼 수 있게 될 것입니다. 만약 현재시점에서 사용 불가능한 복구 블록이 발견되었다면 (i.e. 코드의 현재 블록에서든지 현재 실행 포인터가 위치하고 있는 메소드라든지 등등), 루비는 어울리는 복구 블록을 찾을 수만 있다면 이를 보기 위해 스택 상단으로 호출하게 될 것입니다 (i.e. it works its way from the offending method up to the method called it to the method that called it and so on). 메인 응용프로그램 메소드를 탐색하기 전에 적절한 복구 블록을 찾지 못했다면 응용프로그램은 스레드로부터 빠져나가게 되고 아마 다음과 같은 한줄 "예외 핸들러를 빼먹는 이런 멍청이에게 참 유감이다!" 을 내뱉으며 정적을 깨는 시끄러운 충돌 따위가 뒤따르게 될겁니다... 킁킁...

복구 블록은 상세히 기술된 다양한 에러를 잡기위해 다양한 이름이 만들어졌을때 이에 대해 정의할 수 있습니다. 다시한번 앞의 예제를 인용해보도록 하겠습니다:

def method_of_doom
   my_string = "I sense impending doom."
   my_string.ah_ha_i_called_a_nonexistent_method
rescue NoMethodError => e:
   puts "PROBLEM: " + e.to_s
rescue Exception:
   puts "Uhh...there's a problem with that there method."
end

method_of_doom
→ PROBLEM: undefined method and so on...

Leveraging this ability makes it far easier to know exactly what's going on, where, and sometimes how to fix it.

A rescue block also provides a few other features that aid in making sure that even if your application does choke, it will still hopefully run as smoothly as possible. The first of these features is the else caluse that you can tag on to a rescue block. This block will execute if there are no exceptions raised. For example:

def no_problemo
   x = 0
   x += 19
rescue Exception
   puts "Oh noes!"
else
   puts "All clear!"
end

no_problemo
→ All clear!

This is a useful feature, but be careful what you put in there. The rescue block in the enclosing code won't catch any exceptions raised in the else clause, so you may need to catch them later up the call stack or relegate the else block to menial tasks to avoid the risk of causing worse problems.

The second feature offered by rescue blocks is the ensure clause; this clause holds code that is always executed (i.e. regardless of the presence exceptions or not). For example:

def dance_a_jig
   "I'm a dancin'!"
   "Do si do!"
   rebel_yell = "yee haw!".upcase!
rescue Exception
   print "I fell down, dang it!"
else
   print rebel_yell
ensure
   print " That's all folks!"
end

dance_a_jig
→ YEE HAW! That's all folks!

The ensure clause is always executed no matter what; this construct is very useful for closing files that you have been reading from or writing to, closing open network connections, or making sure all your resource handles have been cleaned up. If you put these sorts of things in ensure clauses, they will always get done and cut down on problems you may have with resource access if your application crashes.

1.3.2. Rescue Statement Modifier

상태제어와 루프문과 거의 유사하게, rescue 블록 구문은 상태 수정자로 쓰일 수 있습니다:

not_an_object.do_something rescue puts "Crash!"
→ Crash!

rescue 를 통해서 어떤 종류의 예외인지 정의할 수 없지만, 아무리해도 "형식적으로" rescue 블록의 왼쪽에 놓인 걸로 하는 것이 낫겠지요. 또한 이러한 구조를 통해 값을 할당할 수 있습니다.

my_value = not_an_object.give_a_value rescue "Burn!"
puts my_value
→ Burn!

이런 싸구려틱(!)한 예제로는 물론 실제 상황의 경우를 보여주지 못하지만 이러한 구조는 대부분의 어떤 예외상황에서도 값을 올바르게 수정할 수 있도록 약간씩 조절할 경우 유용하게 쓰일 수 있습니다(드물지만 가능한 경우에요).

1.3.3. Retry

때론 do-over(좀 쉽게 풀이하자면 동작중 동작 지시라 해야 하겠죠? 가령... 일이 잘못되어 가고 있는 경우 "동작그만! 역할을 재분담하기로 한다" 라고 명령 내리듯이...) 가 필요할 때가 있습니다. 이 반복 형태는 의도하는대로 동작하지 않거나 이러한 상황에서 어떠한 자극을 주어도 변수가 여전히 해결되지 않을 때가 있습니다.(가령 말도 안되는 결과가 계속 출력되고 이런 상황에서 헤어나지 못할때지요.) 어떻든간에 이럴 때는 더 나은 다음 단계로 간단하게 넘어가는 코드를 통한 반복동작이 가능한 부분을 필요로 합니다. 이 동작이 바로 retry 키워드가 위치한 곳 안에서 이루어집니다. 가상의 웹브라우져를 만들었다 칩시다.

def make_request
if (@http11)
   self.send('HTTP/1.1')
else
    self.send('HTTP/1.0')
end
rescue ProtocolError
    @http11 = false
    retry
end
HTTP 1.1헤더를 보냈지만 서버에서는 이를 인식하지 못하고 ProtocolError를 토해냅니다. 상황을 접고 단순 예외/에러로 마무리하는 대신에 HTTP 1.1를 쓰지 못하게 하여 이 상황을 차단합니다. 재 시도 요청을 하여 웹 브라우저로 하여금 능력껏 HTTP 1.0 으로 대신 전환하게 합니다. 좋죠? rescue와 retry를 사용함으로 인해, 예외들과 상황 차단의 재시도를 야기하는 에러들을 수정하게끔 시도하게 할 수 있습니다. 근데 이걸 잘 지켜보면, 만약 이 문제가 해결되지 않고 계속 프로그램에 의해 뱅뱅 도는 무한루프 상태에 빠져들게 된다면 이것이 더욱 이상한 문제를 야기할 수도 있다는걸 아실 수 있게 될 것입니다.

1.3.4. 예외 발생

그래서 이제 예외를 어떻게 다루는지 배운다는건, 필자가 생각하기로는 지금이 바로 당신만의 예외를 활용하기 위해 좋은 때가 아닌가 싶습니다. 예외를 발생하는 것은 루비 자체적으로 문제가 발생하지 않을지도 모르는 이런 이상한 "중요한" 문제가 발생하였을때에 대비하여 필요합니다. (말이 좀 어려운데... 결국 말장난 입니다. -_-;) 예를 들자면, 음.. 다음 Person 클래스에 포함된 메소드를 보도록 하지요.

def define_gender(gender)
   if (gender.upcase != 'FEMALE') && (gender.upcase != 'MALE')
      raise "You specified something wonky for gender!"
   end
end

my_guy = Person.new
my_guy.define_gender("nobody knows")
→ ! RuntimeError ("You specified something wonky for gender!")

비록 이게 Ruby 자체적으로 문제가 되진 않겠지만, 명백하게 따진다면 성별을 위해 비정상적으로 정의하는 것을 원치 않을 것입니다. (게이나 레즈비언이나 -_-; 물론 그렇게 한다면 참 웃긴 일이 되어 버리겠죠). 만약 어떤 상태가 발생하였을때, 메세지를 통해 딸려나오는 줄 상에 키워드를 떨궈놓을 수 있고, 이렇게 함으로 해서 루비가 새로운 RuntimeError를 발생시켜서 런타임 에러에 관한 메세지를 소스상에 제공한 대로 설정합니다. 그게 바로 raise 키워드고 (이게 아니라 고상하지 못한 웨일즈 지방 사촌 말대로라면, fail 키워드 쯤 되겠죠) 이놈은 다양한 방법으로 호출됩니다:

raise "I crashed! This message should be more informative!"
raise
raise NoMethodError, "That method ain\'t here!", caller

처음 방식은 아마 꽤 친숙한 방법일 것입니다. (신종 RuntimeError에 대한 메세지를 제공하죠); 두번째 방식은 정해진 예외가 없다면 현재 예외를 또 발생시켜서 콜 스택에 쌓아두고 넘어가든지 어떤 메세지도 남기지 않고 예외를 발생시켜 종료해버립니다. 마지막 방식은 거의 변함없이 대부분 쓰일텐데 왜냐면 이 형식으로 하여금 예외 유형과 메세지 그리고 스택 추적 객체를 정의할 수 있기 때문입니다. (종종 이걸 단지 호출자, 커널참조자, 호출자 메소드라 하죠). 좋은 프로그램의 습관은 가능한한 모든 정의된 예외가 튀어나옵니다. 항상 RuntimeErrors 예외가 던져지는게 아니라 필요로 하는 것에 매치시키기 위한 예외 유형이나 올바른 유형이 아닌 객체가 제공되었을때 TypeError 예외를 던져볼 수도 있죠. (얘기가 좀 어렵죠? 이 소스코드의 경우 GenderError를 발생시키는 것이 더 좋거나 예시에서와 같이 RuntimeError보다 더 좋은 그 어떤 예외를 던져볼 수 있다는 것입니다. 예외의 명확화...).

1.3.5. 나만의 예외 만들기

그래서 어떻게 사용자 정의 예외 형식을 만들 수 있을까요? 루비에서는 진짜로 쪼꿈 간단합니다. 일단 설명 보기 전에 예제먼저 실행해보도록 하겠습니다. 얼렁!!:

class GenderError < RuntimeError
    attr :what_they_put

    def initialize(their_input)
        @what_they_put = their_input
    end
end

새로운 예제를 만들기 위해, 예외 클래스의 일부를 상속받습니다. (e.g. 필자의 경우는 RuntimeError로부터 상속 받았지만, 독자의 경우에는 TypeError, NoMethodError, 또는 다른 예외 클래스로부터 상속받을 수 있습니다). 해서 코드를 새로 고쳐서 연습삼아 또 다른 새로운 예외를 만들어 보도록 하겠습니다:

class Person
   def define_gender(gender)
      if (gender.upcase != 'FEMALE') && (gender.upcase != 'MALE')
         raise GenderError.new(gender), "Invalid input!"
      end
   end

   def initialize(gender)
      self.define_gender(gender)
   rescue GenderError => bad
      puts "You gave me some bad input: " + bad.what_they_put
      raise
   end
end

my_guy = Person.new("Who knows?")
→ You gave me some bad input: Who knows? → ! GenderError ("Invalid input!")

define_gender에서 예외를 일으켜서 initialize에서 처리한 다음 스택의 위쪽으로 넘긴 것에 주목하세요. initialize안에서 우리가 새로 만든 예외 클래스 내의 속성(attribute)를 출력했습니다. 즉, 예외들도 모두 전부 객체이며 클래스에서 생성되기 때문에 여러분은 자신만의 예외를 만들때 메쏘드와 속성을 집어넣을 수 있습니다. 내가 사용자 입력값을 저장했던 것처럼 말입니다. 이는 예외 핸들러로 더 많은 데이터를 제공하고자 하거나 예외에서 복구하고자 하는 메쏘드를 예외 클래스에서 제공하고 싶을 때 유용합니다.

1.3.6. Throw 와 Catch

C# 이나 Java 를 다루는 프로그래머라면, 이 분야에 있어 약간 친숙하리라 예상되는 바로 이것에 대해 단지 흥분하기에 그치겠지만, 둥지의 이름난 알의 갯수를 세진 못할 것입니다.(갑자기 이게 뭔 소리여 -_-?) 루비에서 catch 블록은 인자와 같은 식별자를 받고 throw를 통해 이 식별자를 결과로서 계속될 코드에 던져버릴 수 있습니다. 루비는 스택에서 맞는 catch 구문이 어떤것인지를 찾고, 이게 발견되었다면, 루비는 멈추고 일반 처리 과정에 진입하고 catch블록에서 빠져나갑니다. 얘기로 하자면 더 어렵기 때문에 다음 예제를 참고하도록 하겠습니다:

princess = DamselInDistress.new
catch :hes_a_failure do
    # YAY! Someone's here to save her...
    print "My prince is here! "

    # OH NO! The villain has eaten his liver! He dies!
    princess.is_saved = false

    if (princess.is_saved == true)
        puts "Hooray!"
    else
        puts "Poo! Not again!"
throw :hes_a_failure

end

puts "I'm going to sleep until the next guy..."
# Nap...
end
→ My prince is here! Poo! Not again!

catch 블록은 catch 키워드가 위치한 곳에서부터 시작됩니다. 식별자가 위치하고 난후 do 키워드가 한줄에 있습니다. 식별자는 throw 키워드와 같이 사용되며 심볼 혹은 문자열 둘다가 될 수 있습니다. 이 코드는 throw 구문에 붙어있는 식별자의 값이 일치할때까지 실행되며 (i.e. throw식별자는 catch의 식별자와 동일합니다) 이에 셈한 결과가 들어가게 됩니다. (만약 1이 세어졌을때) 이의 경우에는 throw 구문을 만난 이후로 그 어떤 구문도 실행하지 않으며 catch 블록으로부터 빠져나가게 됩니다. 예를 들어, 이 같은 일의 경우에는 "My prince is here!" 와 "Poo! Not again!" 가 출력되겠지만 "I'm going to sleep until the next guy..." 는 이와 같은 경우가 아닐때 출력될 것입니다. 블록으로부터 빠져나간 후에도 매칭되는 throw 구문을 (i.e. throw :hes_a_failure) 만나게 됩니다. 이러한 구조는 중첩된 루프로 하여금 깊이 묻혀 있거나 에러를 발생하는 코드의 복잡한 블록으로 부터 빠져나갈때 매우 유용하며 심지어는 이러한 코드의 중단을 빠르고 쉽게 하기 위해 필요합니다.

1.4. This Chapter

뭘 배웠나 알아볼까용?...

  • if/unless 문과 상태 연산자에 대해...
  • 루프 구분, 두 상태와 반복, 더욱 효과적으로 제어하는 방법.
  • 예외와 사용방법.



sponsored by andamiro
sponsored by cdnetworks
sponsored by HP

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2011-04-20 16:09:00
Processing time 0.0180 sec