메뉴

루비 블럭, Procs, Lambdas 이해하기

2016-01-16 15:44:51

목차

소개

루비의 블럭, Procs, 람다(Lambdas)는 강력한, 때로는 마법처럼 보이기도 하는 기능이지만 이해하기 어려운 기능이기도 하다. 다른 언어에서 쉽게 찾아볼 수 있는 기능이 아니기 때문이다. 특히 C, C++, Java, PHP등의 언어를 기본으로 하고 있다면 더욱 그렇다. 이들 언어는 클로저(Closure)라는 프로그래밍 개념이 없기 때문이다. 루비의 블럭, procs, 람다는 클로저를 바탕으로 하고 있기 때문에, 클로저 개념에 익숙해질 필요가 있다.

블럭

루비에서 클로저를 이용하는 가장 쉬운 방법은 "블럭"을 이용하는 거다. 다음은 루비 블럭의 사용예다.

array = [1,2,3,4,5]

array.collect! do | n |
    n ** 2
end

puts array.inspect  # => [1, 4, 9, 16, 25]
어떤 일이 일어나는지 확인해 보자.
  1. 배열과 collect! 메서드를 블럭에 있는 코드로 보낸다.
  2. 코드 블럭은 (블럭에 연결된)collect! 메서드를 이용해서 변수의 값을 가져와서 코드를 적용한다. 이 경우 제곱 연산을 한다.
  3. 제곱연산을 한 값을 배열에 넣는다.
collect! 메서드와 block를 간단하게 연결했다. 이제 block의 코드를 이용해서 배열의 원소들을 가져와서 필요한 연산을 할 수 있다.

collect!와 비슷한 일을 하는 메서드를 직접 만들어보자. 대략 다음과 같은 형태가 될거다.
class Array
  def iterate!
    self.each_with_index do |n, i|
      self[i] = yield(n)
    end
  end
end

array = [1, 2, 3, 4]

array.iterate! do |n|
  n ** 2
end

puts array.inspect

# => [1, 4, 9, 16]
Array 클래스를 열어서 iterate! 라는 메서드를 추가했다. 블럭의 코드를 수행하는 부분은 yield(n)으로, n**2를 수행해서 그 결과를 self![i!]에 입력한다.

어떤 일이 일어나는지를 추적해 보자.
  1. 배열의 원소를 iterator! 메서드에 보낸다.
  2. yield는 원소의 값 'n'을 매개변수로 호출하는데, 이 때 블럭에 있는 코드 n ** 2를 수행하게 된다. 결국 배열의 원소들에 대한 제곱연산을 수행하게 된다.
  3. 블럭 수행 결과 즉 제곱연산 결과는 메서드 내부로 전달되고, 배열에 다시 쓰게 된다.
  4. 배열의 모든 원소에 대해서 이 과정이 반복된다.

Procedures, AKA, Procs

블럭은 이해하기 쉽운데다가 간단히 사용할 수 있다. 하지만 만약 블럭에 있는 코드를 여러 곳에서 사용하려고 하면, 동일한 블럭문을 copy & paste 해야 한다. 깔끔하지 않다. 다음의 코드를 보자.
class Array
  def iterate!
    self.each_with_index do |n, i|
      self[i] = yield(n)
    end
  end
end

array1 = [1,2,3,4,5]
array2 = [6,7,8,9,10]


array1.iterate! do | n |
    n ** 2
end

array2.iterate! do | n |
    n ** 2
end

puts array1.inspect
puts array2.inspect
동일한 코드를 여러 군데 쓰는 건 좋은 프로그래밍 습관이 아니다.

이런 문제는 코드를 재 사용하는 것으로 해결할 수 있다. 루비에서는 이런 재 사용 가능한 코드를 Proc(Procedure의 줄임말이다.)라고 부른다. Proc와 블럭의 다른 점이라면, 블럭은 한번만 사용하기 위해서 적당한 방법이고 Proc는 재 사용이 가능하다라는 점이다. 위 코드를 Proc를 이용해서 다듬었다.
class Array
  def iterate!(code)
    self.each_with_index do |n, i|
      self[i] = code.call(n)
    end
  end
end

array1 = [1,2,3,4,5]
array2 = [6,7,8,9,10]

square = Proc.new do |n|
    n ** 2
end

array1.iterate!(square)
array2.iterate!(square)

puts array1.inspect
puts array2.inspect

실행 결과
[1, 4, 9, 16, 25]
[36, 49, 64, 81, 100]

Lambdas

Procs를 이용하는 두 가지 방법이 있다. 첫 번째 방식은 위에서 설명했고, 두번째 방식은 메서드의 매개변수 형태로 코드를 직접 넘기는 방식이다. 자바스크립트에서는 anonymous function이라는 이름으로 부르고 있다.
class Array
  def iterate!(code)
    self.each_with_index do |n, i|
      self[i] = code.call(n)
    end
  end
end

array1 = [1,2,3,4,5]

array1.iterate!(lambda {|n| n ** 2})

puts array1.inspect  # [1, 4, 9, 16, 25]

Procs와 비슷해 보인다. 몇 가지 조그만 차이가 있는데, lambda는 Procs와 달리 매개변수의 갯수를 검사는 점이다.
def args(code)
  one, two = 1, 2
  code.call(one, two)
end
args(Proc.new{|a, b, c| puts "Give me a #{a} and a #{b} and a #{c.class}"})
args(lambda{|a, b, c| puts "Give me a #{a} and a #{b} and a #{c.class}"})

실행결과
Give me a 1 and a 2 and a NilClass
lambda.rb:8:in `block in <main>': wrong number of arguments (2 for 3) (ArgumentError)
        from lambda.rb:3:in `call'
        from lambda.rb:3:in `args'
        from lambda.rb:8:in `<main>'
Proc의 경우 추가적인 변수는 nil로 설정하는 걸 볼 수 있다. 반면 lambda는 에러를 반환한다.

두번째 다른 점은 lambda는 diminutive return을 할 수 있다는 점이다. 예컨데, Proc는 값을 반환하면 메서드를 중단한다. 따라서 메서드의 최종 반환 값은 Proc의 반환값이 된다. 하지만 lambda는 반환 후에도 메서드가 계속 된다. 다음 예를 보자.
def proc_return
  Proc.new { return "Proc.new"}.call
  return "proc_return method finished"    # 실행하지 않는다.
end

def lambda_return
  lambda { return "lambda" }.call
  return "lambda_return method finished" # 실행된다.
end

puts proc_return
puts lambda_return

실핼결과
Proc.new
lambda_return method finished

Lambda는 아래와 같이 lambda의 반환 값을 받아서, 메서드에서 처리하는게 가능하다.
def lambda_return
  a = lambda { return "lambda" }.call
  return "#{a} lambda_return method finished"
end

이러한 차이가 발생하는 이유는 루비의 Proc는 code snippet형태로 작동하기 때문이다. 메서드는 Proc의 return 코드를 수행하게되고, 따라서 다음 코드를 수행하지 않고 바로 반환해 버린다.

Lambda와 Proc의 차이점을 보여주는 다른 예제다.
def generic_return(code)
  code.call
  return "generic_return method finished"
end

puts generic_return(Proc.new { return "Proc.new" })
puts generic_return(lambda { return "lambda" })

실행 결과
ruby lambda.rb 
lambda.rb:6:in `block in <main>': unexpected return (LocalJumpError)
        from lambda.rb:2:in `call'
루비는 매개변수로 return 키워드를 가지는 코드를 허용하지 않기 때문에 에러가 발생한다. Lambda는 코드가 아닌 리턴값 "lambda"를 매개변수로 넘겨주기 때문에 문제없이 동작한다.

def generic_return(code)
  one, two    = 1, 2
  three, four = code.call(one, two)
  return "Give me a #{three} and a #{four}"
end

puts generic_return(lambda { |x, y| return x + 2, y + 2 })   #=> Give me a 3 and a 4
puts generic_return(Proc.new { |x, y| return x + 2, y + 2 }) #=> 에러 
puts generic_return(Proc.new { |x, y| x + 2; y + 2 })        #=> Give me a 4 and a 
puts generic_return(Proc.new { |x, y| [x + 2, y + 2] })      #=> Give me a 3 and a 4
코드가 제대로 작동을 한다면 generic_return은 두 개의 값을 반환해야 한다.
  1. lambda코드를 적용한 첫번째 코드 : 원하는 결과를 보여준다.
  2. return을 포함한 두번째 코드 : 키워드로 return이 들어가 있다. 에러 발생
  3. return을 포함하지 않은 코드 : 에러가 생기진 않지만, 하나만 리턴.
  4. 배열을 사용하면 된다.

Method Object

이미 만들어놓은 메서드를 다른 메서드의 closure로 사용하고 있다면, 루비의 method 메서드를 이용하면 된다.
class Array
  def iterate!(code)
    self.each_with_index do |n, i|
      self[i] = code.call(n)
    end
  end
end

def square(n)
  n ** 2
end

array = [1, 2, 3, 4]
array.iterate!(method(:square))
puts array.inspect

실행결과
[1, 4, 9, 16]

method(:square)는 Proc가 아닌 Method다. 확인해 보자.
puts method(:square).class  #=> Method
그냥 lambda와 같은 역할을 한다고 보면 되겠다.

참고