제어 표현식

제어 표현식 (Control Expressions)

Ruby에는 실행 흐름을 제어하는 여러 방법이 있어요. 여기서 설명하는 모든 표현식은 값을 돌려줘요.

이 제어 표현식들의 검사에서 nilfalse는 거짓값(false-value)이고, true와 그 외의 모든 객체는 참값(true-value)이에요. 이 문서에서 "참"은 "참값"을, "거짓"은 "거짓값"을 뜻해요.

출처: Ruby 공식 문서

본문

if 표현식 (Expression)

가장 단순한 if 표현식은 "test" 표현식과 "then" 표현식, 두 부분으로 이뤄져요. "test" 표현식이 참으로 평가되면 "then" 표현식이 평가돼요.

간단한 if 문장을 볼게요.

if true then
  puts "the test resulted in a true-value"
end

이 코드는 "the test resulted in a true-value"를 출력해요.

then은 선택 사항이에요.

if true
  puts "the test resulted in a true-value"
end

이 문서에서는 if의 가장 흔한 사용법에 맞춰 모든 표현식에서 선택 사항인 then을 생략할게요.

else 표현식을 추가할 수도 있어요. test가 참으로 평가되지 않으면 else 표현식이 실행돼요.

if false
  puts "the test resulted in a true-value"
else
  puts "the test resulted in a false-value"
end

이 코드는 "the test resulted in a false-value"를 출력해요.

elsif를 사용해서 if 표현식에 추가 검사들을 원하는 만큼 붙일 수 있어요. elsif는 그 elsif보다 위에 있는 모든 검사가 거짓일 때 실행돼요.

a = 1

if a == 0
  puts "a is zero"
elsif a == 1
  puts "a is one"
else
  puts "a is some other value"
end

1은 0과 같지 않으므로 이 코드는 "a is one"을 출력해요. else는 조건이 하나도 맞지 않을 때만 실행되니까요.

조건 하나가 맞으면(if 조건이든 elsif 조건이든), 그 if 표현식은 끝나고 더 이상의 검사는 수행되지 않아요.

if처럼 elsif 조건 뒤에도 then이 올 수 있어요.

다음 예시에서는 "a is one"만 출력돼요.

a = 1

if a == 0
  puts "a is zero"
elsif a == 1
  puts "a is one"
elsif a >= 1
  puts "a is greater than or equal to one"
else
  puts "a is some other value"
end

ifelsif의 검사에는 부수 효과(side-effect)가 있을 수 있어요. 부수 효과의 가장 흔한 쓰임은 값을 지역 변수에 캐시하는 거예요.

if a = object.some_value
  # do something to a
end

if 표현식의 결과값은 표현식에서 마지막으로 실행된 값이에요.

삼항 if (Ternary if)

?:를 사용해서 if-then-else 표현식을 쓸 수도 있어요. 이 삼항 if는:

input_type = gets =~ /hello/i ? "greeting" : "other"

if 표현식과 같아요.

input_type =
  if gets =~ /hello/i
    "greeting"
  else
    "other"
  end

삼항 if는 더 장황한 형태보다 훨씬 짧게 쓸 수 있지만, 가독성을 위해 간단한 조건문에서만 쓰는 걸 권해요. 또 같은 표현식 안에서 삼항 조건을 여러 개 쓰는 것도 피하세요. 혼란스러울 수 있으니까요.

unless 표현식 (Expression)

unless 표현식은 if 표현식의 반대예요. 값이 거짓이면 "then" 표현식이 실행되죠.

unless true
  puts "the value is a false-value"
end

true는 거짓값이 아니므로 이 코드는 아무것도 출력하지 않아요.

if처럼 unless에도 선택 사항인 then을 쓸 수 있어요.

위의 unless 표현식이 다음 코드와 같다는 점을 기억하세요.

if not true
  puts "the value is a false-value"
end

if 표현식처럼 unless에도 else 조건을 쓸 수 있어요.

unless true
  puts "the value is false"
else
  puts "the value is true"
end

이 코드는 else 조건에서 "the value is true"를 출력해요.

unless 표현식에는 elsif를 쓸 수 없어요.

unless 표현식의 결과값은 표현식에서 마지막으로 실행된 값이에요.

수식어 if와 unless (Modifier if and unless)

ifunless는 표현식을 수식하는 데도 쓸 수 있어요. 수식어로 쓰면 왼쪽이 "then" 문장이 되고 오른쪽이 "test" 표현식이 돼요.

a = 0

a += 1 if a.zero?

p a

이 코드는 1을 출력해요.

a = 0

a += 1 unless a.zero?

p a

이 코드는 0을 출력해요.

수식어 형태와 표준 형태 모두 "test" 표현식과 "then" 문장을 갖고 있지만, 파싱 순서 때문에 서로 정확히 같은 변환은 아니에요. 차이를 보여주는 예시를 볼게요.

p a if a = 0.zero?

이 코드는 NameError "undefined local variable or method 'a'"를 발생시켜요.

ruby가 이 표현식을 파싱할 때 먼저 "then" 표현식에서 a를 메서드 호출로 만나고, 나중에 "test" 표현식에서 a에 대한 할당을 보고 a를 지역 변수로 표시해요.

이 줄을 실행할 때는 먼저 "test" 표현식인 a = 0.zero?를 실행해요.

test가 참이므로 "then" 표현식인 p a가 실행돼요. 본문의 a는 존재하지 않는 메서드로 기록되어 있었으므로 NameError가 발생해요.

unless도 마찬가지예요.

case 표현식 (Expression)

case 표현식은 두 가지 방식으로 쓸 수 있어요.

가장 흔한 방식은 객체를 여러 패턴과 비교하는 거예요. 패턴은 Object에서 ==에 별명이 붙어 있는 === 메서드로 매칭돼요. 다른 클래스는 의미 있는 동작을 주려면 이 메서드를 오버라이드해야 해요. 예시는 Module#===Regexp#===을 참고해요.

case로 String을 패턴과 비교하는 예시를 볼게요.

case "12345"
when /^1/
  puts "the string starts with one"
else
  puts "I don't know what the string starts with"
end

여기서 문자열 "12345"/^1/ === "12345"를 호출해서 /^1/과 비교돼요. 이 호출은 true를 돌려주죠. if 표현식처럼, 첫 번째로 매치되는 when이 실행되고 나머지 매치는 무시돼요.

매치되는 게 없으면 else가 실행돼요.

elsethen은 선택 사항이에요. 다음 case 표현식은 위와 같은 결과를 내요.

case "12345"
when /^1/
  puts "the string starts with one"
end

같은 when에 여러 조건을 둘 수 있어요.

case "2"
when /^1/, "2"
  puts "the string starts with one or is '2'"
end

Ruby는 각 조건을 차례로 시도해요. 그래서 처음에 /^1/ === "2"false를 돌려주고, 다음에 "2" === "2"true를 돌려줘서 "the string starts with one or is '2'"가 출력돼요.

when 조건 뒤에 then을 쓸 수 있어요. 이건 when 본문을 한 줄에 놓을 때 가장 자주 쓰여요.

case a
when 1, 2 then puts "a is one or two"
when 3    then puts "a is three"
else           puts "I don't know what a is"
end

case 표현식을 쓰는 다른 방식은 if-elsif 표현식처럼 쓰는 거예요.

a = 2

case
when a == 1, a == 2
  puts "a is one or two"
when a == 3
  puts "a is three"
else
  puts "I don't know what a is"
end

여기서도 thenelse는 선택 사항이에요.

case 표현식의 결과값은 표현식에서 마지막으로 실행된 값이에요.

Ruby 2.7부터 case 표현식은 in 키워드를 통해 더 강력한 실험적(experimental) 패턴 매칭 기능도 제공해요.

case {a: 1, b: 2, c: 3}
in a: Integer => m
  "matched: #{m}"
else
  "not matched"
end
# => "matched: 1"

패턴 매칭 문법은 별도 페이지에 설명되어 있어요.

while 루프 (Loop)

while 루프는 조건이 참인 동안 실행돼요.

a = 0

while a < 10 do
  p a
  a += 1
end

p a

이 코드는 0부터 10까지의 숫자를 출력해요. 조건 a < 10은 루프에 들어가기 전에 검사되고, 그다음 본문이 실행되고, 그다음 조건이 다시 검사돼요. 조건이 거짓이 되면 루프가 종료돼요.

do 키워드는 선택 사항이에요. 다음 루프는 위의 루프와 같아요.

while a < 10
  p a
  a += 1
end

while 루프의 결과는 값 제공을 위해 break를 쓰지 않는 한 nil이에요.

until 루프 (Loop)

until 루프는 조건이 거짓인 동안 실행돼요.

a = 0

until a > 10 do
  p a
  a += 1
end

p a

이 코드는 0부터 11까지의 숫자를 출력해요. while 루프처럼 조건 a > 10은 루프에 들어갈 때와 루프 본문이 실행될 때마다 검사돼요. 조건이 거짓이면 루프가 계속 실행돼요.

while 루프와 마찬가지로 do는 선택 사항이에요.

while 루프와 마찬가지로 until 루프의 결과도 break를 쓰지 않는 한 nil이에요.

for 루프 (Loop)

for 루프는 for 다음에 반복 인자를 담을 변수, 그다음 in, 그리고 each로 반복할 값을 두는 것으로 이뤄져요. do는 선택 사항이에요.

for value in [1, 2, 3] do
  puts value
end

이 코드는 1, 2, 3을 출력해요.

whileuntil처럼 do는 선택 사항이에요.

for 루프는 each를 쓰는 것과 비슷하지만, 새로운 변수 스코프를 만들지 않아요.

for 루프의 결과값은 break를 쓰지 않는 한 반복된 값이에요.

for 루프는 현대 Ruby 프로그램에서는 좀처럼 쓰이지 않아요.

수식어 while과 until (Modifier while and until)

ifunless처럼 whileuntil도 수식어로 쓸 수 있어요.

a = 0

a += 1 while a < 10

p a # prints 10

수식어로 쓰인 until:

a = 0

a += 1 until a > 10

p a # prints 11

beginend를 사용하면 본문을 조건보다 먼저 한 번 실행하는 while 루프를 만들 수 있어요.

a = 0

begin
  a += 1
end while a < 10

p a # prints 10

rescueensure를 쓰지 않으면 Ruby는 예외 처리 오버헤드를 최적화해서 없애줘요.

break 문 (Statement)

블록을 일찍 벗어나려면 break를 써요. 다음 코드는 values의 항목 중 하나가 짝수이면 반복을 멈춰요.

values.each do |value|
  break if value.even?

  # ...
end

while 루프에서도 break로 종료할 수 있어요.

a = 0

while true do
  p a
  a += 1

  break if a < 10
end

p a

이 코드는 숫자 0과 1을 출력해요.

break는 값 하나를 받아서, 그것이 "빠져나가는" 표현식의 결과값으로 제공돼요.

result = [1, 2, 3].each do |value|
  break value * 2 if value.even?
end

p result # prints 4

next 문 (Statement)

현재 반복의 나머지를 건너뛰려면 next를 써요.

result = [1, 2, 3].map do |value|
  next if value.even?

  value * 2
end

p result # prints [2, nil, 6]

next는 인자를 받아서 그것을 현재 블록 반복의 결과값으로 쓸 수 있어요.

result = [1, 2, 3].map do |value|
  next value if value.even?

  value * 2
end

p result # prints [2, 2, 6]

redo 문 (Statement)

현재 반복을 다시 하려면 redo를 써요.

result = []

while result.length < 10 do
  result << result.length

  redo if result.last.even?

  result << result.length + 1
end

p result

이 코드는 [0, 1, 3, 3, 5, 5, 7, 7, 9, 9, 11]을 출력해요.

Ruby 1.8에서는 redo를 쓰는 자리에 retry도 쓸 수 있었어요. 이제는 그렇지 않아요. 이제 rescue 블록 밖에서 retry를 쓰면 SyntaxError를 받게 돼요. retry의 올바른 사용법은 예외(Exceptions) 문서를 참고해요.

수식어 문장 (Modifier Statements)

Ruby의 문법은 문장(statement)과 표현식(expression)을 구분해요. 모든 표현식은 문장이지만(표현식은 문장의 한 종류죠), 모든 문장이 표현식인 것은 아니에요. 문법의 어떤 부분은 표현식을 받고 다른 종류의 문장은 받지 않는데, 이 때문에 비슷해 보이는 코드가 다르게 파싱되기도 해요.

예를 들어 수식어로 쓰이지 않을 때 if, else, while, until, begin은 표현식(그래서 문장이기도 하죠)이에요. 그러나 수식어로 쓰일 때 if, else, while, until, rescue는 문장이지만 표현식은 아니에요.

if true; 1 end # expression (and therefore statement)
1 if true      # statement (not expression)

표현식이 아닌 문장은 메서드 인자 같은 표현식이 기대되는 자리에서는 쓸 수 없어요.

puts( 1 if true )      #=> SyntaxError

문장을 괄호로 감싸면 표현식으로 만들 수 있어요.

puts((1 if true))      #=> 1

메서드 이름과 여는 괄호 사이에 공백을 두면 괄호를 두 겹 쓸 필요가 없어요.

puts (1 if true)       #=> 1, because of optional parentheses for method

이건 괄호 없는 메서드 호출처럼 파싱되기 때문이에요. 다음 코드와 같되 지역 변수만 만들지 않는 거예요.

x = (1 if true)
p x

수식어 문장에서 왼쪽은 문장이어야 하고 오른쪽은 표현식이어야 해요.

그래서 a if b rescue c에서 b rescue c는 표현식이 아닌 문장이므로 if 수식어 문장의 오른쪽으로 허용되지 않아요. 그 코드는 필연적으로 (a if b) rescue c로 파싱돼요.

이것은 연산자 우선순위와 다음과 같이 상호작용해요.

stmt if v = expr rescue x
stmt if v = expr unless x

이들은 다음과 같이 파싱돼요.

stmt if v = (expr rescue x)
(stmt if v = expr) unless x

수식어 rescue=보다 우선순위가 높고, 수식어 if=보다 우선순위가 낮기 때문이에요.

플립플롭 (Flip-Flop)

플립플롭은 약간 특별한 조건 표현식이에요. 대표적인 쓰임 중 하나는 ruby -n이나 ruby -p를 쓰는 ruby 한 줄 프로그램에서 텍스트를 처리하는 거예요.

플립플롭의 형태는 플립플롭이 켜지는 시점을 나타내는 표현식, ..(또는 ...), 그리고 플립플롭이 꺼지는 시점을 나타내는 표현식으로 이뤄져요. 플립플롭이 켜져 있는 동안에는 계속 true로 평가되고, 꺼져 있으면 false로 평가돼요.

예시를 볼게요.

selected = []

0.upto 10 do |value|
  selected << value if value==2..value==8
end

p selected # prints [2, 3, 4, 5, 6, 7, 8]

위 예시에서 '켜짐' 조건은 n==2예요. 플립플롭은 처음에 0과 1에 대해 '꺼짐'(false) 상태였다가 2에서 '켜짐'(true)이 되고 8까지 켜진 상태를 유지해요. 8 이후에는 꺼지고 9와 10에서는 '꺼짐' 상태를 유지해요.

플립플롭은 !, ? :, not, if, while, unless, until 등 수식어 형태를 포함한 조건 안에서만 사용해야 해요.

포함 범위(..)를 쓰면 '켜짐' 조건이 바뀔 때 '꺼짐' 조건이 평가돼요.

selected = []

0.upto 5 do |value|
  selected << value if value==2..value==2
end

p selected # prints [2]

여기서는 플립플롭의 양쪽이 모두 평가되어, 플립플롭은 value가 2와 같을 때만 켜졌다 꺼져요. 플립플롭이 해당 반복에서 켜졌으므로 true를 돌려줘요.

배타 범위(...)를 쓰면 '꺼짐' 조건이 다음 반복에서 평가돼요.

selected = []

0.upto 5 do |value|
  selected << value if value==2...value==2
end

p selected # prints [2, 3, 4, 5]

여기서 플립플롭은 value가 2와 같을 때 켜지지만 같은 반복에서는 꺼지지 않아요. '꺼짐' 조건은 다음 반복이 되어서야 평가되는데, value는 다시 2가 되지 않을 거예요.

throw/catch

throwcatch는 Ruby에서 비지역 제어 흐름(non-local control flow)을 구현하는 데 쓰여요. 예외와 비슷하게 동작해서, throw가 호출된 지점에서 매치되는 catch가 호출된 지점으로 제어가 직접 넘어가요. throw/catch와 예외 사용의 주요 차이는, throw/catch는 예상되는 비지역 제어 흐름을 위해 설계된 반면 예외는 예기치 않은 오류 처리 같은 예외적인 제어 흐름 상황을 위해 설계되었다는 거예요.

throw를 사용할 때는 인자를 1~2개 제공해요. 첫 번째 인자는 매치되는 catch를 위한 값이에요. 두 번째 인자는 선택 사항(기본값 nil)으로, catch 블록 안에서 매치되는 throw가 있으면 catch가 돌려줄 값이에요. catch 블록 안에서 매치되는 throw 메서드가 호출되지 않으면, catch 메서드는 전달받은 블록의 반환값을 돌려줘요.

def a(n)
  throw :d, :a if n == 0
  b(n)
end

def b(n)
  throw :d, :b if n == 1
  c(n)
end

def c(n)
  throw :d if n == 2
end

4.times.map do |i|
  catch(:d) do
    a(i)
    :default
  end
end
# => [:a, :b, nil, :default]

throw에 전달한 첫 번째 인자를 매치되는 catch가 처리하지 못하면 UncaughtThrowError 예외가 발생해요. throw/catch는 예상된 제어 흐름 변경에만 쓰여야 하므로, 이미 예상되지 않은 값을 사용하는 것은 오류라는 뜻이에요.

throw/catch는 키워드가 아니라 Kernel 메서드(Kernel#throwKernel#catch)로 구현돼 있어요. 그래서 BasicObject 컨텍스트에 있으면 직접 사용할 수 없어요. 이 경우 Kernel.throwKernel.catch를 사용할 수 있어요.

BasicObject.new.instance_exec do
  def a
    b
  end

  def b
    c
  end

  def c
    ::Kernel.throw :d, :e
  end

  result = ::Kernel.catch(:d) do
    a
  end
  result # => :e
end

더 알아보기 (Learn more)