루비 메타프로그래밍 Ruby

지난 번 포스팅에서 얘기했던 MetaKoans라는 문제를 주말에 짬짬히 시간날 때마다 풀어보았다. 나는 koan_7까지는 통과했는데 koan_8에서 막혀서 진도가 안나가 이번 퀴즈는 제출을 포기했다. 총 9개의 koans가 있는데, 그 중 마지막 두 개는 클래스 변수를 액세스 하는 것과 상속시에도 변수 액세스가 가능하도록 하는 것이었다.

이번 문제는 knowledge.rb라는 파일 내에 attribute라는 메소드를 만들어 파라메터로 주어지는 변수명을 가지고, 가령 예를 들어 변수명이 a라고 할 때, a, a=, a? 세 개의 액세스 메소드들을 자동으로 생성해 줘야 한다. 우선 처음 다섯 개부터 한 번 천천히 살펴보자.
#
# 'attribute' must provide getter, setter, and query to instances
#
  def koan_1
    c = Class::new {
      attribute 'a'
    }

    o = c::new

    assert{ not o.a? }
    assert{ o.a = 42 }
    assert{ o.a == 42 }
    assert{ o.a? }
  end
#
# 'attribute' must provide getter, setter, and query to classes
#
   def koan_2
    c = Class::new {
      class << self
        attribute 'a'
      end
    }

    assert{ not c.a? }
    assert{ c.a = 42 }
    assert{ c.a == 42 }
    assert{ c.a? }
  end
#
# 'attribute' must provide getter, setter, and query to modules at module
# level
#
  def koan_3
    m = Module::new {
      class << self
        attribute 'a'
      end
    }

    assert{ not m.a? }
    assert{ m.a = 42 }
    assert{ m.a == 42 }
    assert{ m.a? }
  end
#
# 'attribute' must provide getter, setter, and query to modules which operate
# correctly when they are included by or extend objects
#
   def koan_4
    m = Module::new {
       attribute 'a'
    }

    c = Class::new {
       include m
       extend m
    }

    o = c::new

    assert{ not o.a? }
    assert{ o.a = 42 }
    assert{ o.a == 42 }
    assert{ o.a? }

    assert{ not c.a? }
    assert{ c.a = 42 }
    assert{ c.a == 42 }
    assert{ c.a? }
  end
#
# 'attribute' must provide getter, setter, and query to singleton objects
#
   def koan_5
    o = Object::new

    class << o
      attribute 'a'
    end

    assert{ not o.a? }
    assert{ o.a = 42 }
    assert{ o.a == 42 }
    assert{ o.a? }
  end
이 다섯 개의 koan들은 비교적 쉽게 해결된다. 아래 소스를 보자.
# knowledge.rb v1
class Module
  def attribute(x)
    ivar = "@#{x}"
    attr_accessor x
    define_method("#{x}?") { instance_variable_get(ivar) != nil }
  end
end
모듈 클래스의 메소드인 attr_accessor를 부르면 우선 a와 a= 두 개의 메소드는 바로 해결이 된다. 다음 a?는 define_method를 이용해 @a 인스턴스 변수 값을 얻어 그것이 nil인지를 비교하도록하면 해결된다. 이 방법은 위에서 보듯이 attribute가 인스턴스 상황, 클래스 상황, 모듈 상황, 심지어 싱글톤 개체 상황에서 불려도 모두 적용된다. 다음 koan_6을 보자.
#
# 'attribute' must provide a method for providing a default value as hash
#
   def koan_6
    c = Class::new {
       attribute 'a' => 42
    }

    o = c::new

    assert{ o.a == 42 }
    assert{ o.a? }
    assert{ (o.a = nil) == nil }
    assert{ not o.a? }
  end
koan_6는 변수명과 디폴트 값을 해시 형태로 전달해 주는 것에 대한 처리이다. 기본적으로 디폴트 값에 대한 처리인데, 나는 initialize 메소드를 새로 정의하고 그 안에서 값을 대입하도록 함으로써 인스턴스가 새로 생길 때 디폴트 값을 갖도록 하는 방법을 썼었다. 일단 initialize라는 메소드를 마음대로 정의해서 쓰는 것 자체도 꺼림칙 했는데, 이런 방법은 koan_7까지는 어떻게 해결이 되는데 그 이후는 대책이 없다. 다른 사람들이 푼 방법 중 가장 맘에 드는 것을 보자.
# knowledge.rb v2
class Module
  def attribute(x)
    name, value = x.to_a[0]
    ivar = "@#{name}"
    define_method(name) do
       if instance_variables.include?(ivar)
        instance_variable_get(ivar)
       else
        value
       end
    end
    attr_writer name
    define_method("#{name}?") { !!send(name) }
  end
end
우선 맨 처음에 전달된 x에 대해 to_a 메시지를 보내서 스트링이든 해시든 일단 어레이로 변환을 하고, 그 첫번째 값을 가지고 온다. 이것을 name, value 두 값에 대입하는데, 해시라면 key, value 쌍이 대입되겠고 스트링이라면 string, nil이 대입될 것이다. 이렇게 해서 변수명과 디폴트 값을 어떤 경우라도 한 번에 가져올 수 있게 한다.

그 다음에 앞의 버전과 변경된 것은 변수를 읽어내는 메소드이다. 아까는 attr_accessor를 통해 자동으로 만들어 냈는데, 여기서는 새로이 정의를 해주고 대신 attr_writer로 쓰기 메소드만 자동으로 만들어 낸다. 읽기 메소드를 보면 먼저 현재 컨텍스트에서 ivar로 정의된 인스턴스 변수가 존재하는지 보고, 존재하면 그 변수 값을, 존재하지 않으면 디폴트 값을 돌려준다. 왜 이 생각을 못했을까? 나는 처음에 정의할 때 인스턴스 변수에 디폴트 값을 써줄 생각만 했고, 그러다보니 initialize 메소드를 정의해 거기서 변수에 디폴트 값을 써넣을 생각을 하게 됐고, 그러다보니 다음 koan에서 다룰 블럭 처리와 그 이후 클래스 인스턴스 변수의 경우 어려움을 겪게 됐다. 얼마나 간단한가? 그냥 현재 컨텍스트에 변수가 이미 존재하면 돌려주고 존재 안하면 디폴트 값을 돌려주고. 어차피 a=에 의해 새 값이 쓰여지면 그 때 변수는 새로 만들어질 것이고 그걸로 해결인데... 문제를 문제 그대로 풀려고 하지 말고 그것이 의미하는 바가 뭔가를 한 번 더 생각해야 함을 일깨워준다.

마지막으로 a? 메소드를 보면 !!send(name)이라고 정의되어 있다. 재밌는 축약 방법이다. send의 결과가 무엇이든, 그것이 nil이면 최종적으로 false를, nil이 아닌 값이면 true를 돌려주게 하는 방법이다.

이제 koan_7을 보자.
#
# 'attribute' must provide a method for providing a default value as block
# which is evaluated at instance level
#
  def koan_7
    c = Class::new {
       attribute('a'){ fortytwo }
       def fortytwo
        42
       end
    }

    o = c::new

    assert{ o.a == 42 }
    assert{ o.a? }
    assert{ (o.a = nil) == nil }
    assert{ not o.a? }
  end
이번에는 디폴트 값을 해시로 주는 것이 아니라 블럭을 넘겨주고 그 블럭의 리턴 값을 디폴트 값으로 삼아야 하는 문제다. 원래 내 방법대로 하자니 나는 블럭 코드를 @@block이라는 클래스 변수에 넣어 놓고 이것을 나중에 initialize에서 instance_eval이라는 메소드를 사용해 값을 받아내 변수 디폴트 값으로 저장하는 복잡한 방법을 사용했었다. 여기서는 앞의 v2 코드를 확장해 간단히 해결하는 방법을 보자.
# knowledge.rb v3
class Module
   def attribute(x, &block)
    name, value = x.to_a[0]
    ivar = "@#{name}"
    define_method(name) do
       if instance_variables.include?(ivar)
        instance_variable_get(ivar)
       else
        value || (instance_eval &block if block)
       end
    end
    attr_writer name
    define_method("#{name}?") { !!send(name) }
  end
end
여기서 변경된 것은 처음 전달받는 인자에 &block이 추가된 것과 읽기 메소드안에 인스턴스 변수가 정의 안되어 있을 때 이전에는 해시로 전달받은 value 값을 디폴트로 돌려줬는데, 여기서는 그 value가 정의 안되어 있다면 (즉, 처음에서 해시 형태가 아니라 변수명만 넘어온 경우) block이 정의 되어 있는지를 확인하고 정의 되어 있으면 그 블럭의 결과 값을 돌려주게 한 것이다. 간단하고 깔끔하게 추가되었다.

재밌는 것은 여기까지만 해결해 주면 나머지 koan_8과 koan_9는 알아서 해결이 된다는 것이다. 나는 koan_8에서 막혀서 진전이 안됐었는데 ㅠㅠ 아무튼 koan_8, 9는 아래와 같다.
#
# 'attribute' must provide inheritance of default values at both class and
# instance levels
#
  def koan_8
    b = Class::new {
       class << self
        attribute 'a' => 42
        attribute('b'){ a }
       end
       attribute 'a' => 42
       attribute('b'){ a }
    }

    c = Class::new b

    assert{ c.a == 42 }
    assert{ c.a? }
    assert{ (c.a = nil) == nil }
    assert{ not c.a? }

    o = c::new

    assert{ o.a == 42 }
    assert{ o.a? }
    assert{ (o.a = nil) == nil }
    assert{ not o.a? }
    end
#
# into the void
#
   def koan_9
    b = Class::new {
       class << self
        attribute 'a' => 42
        attribute('b'){ a }
       end
       include Module::new {
        attribute 'a' => 42
        attribute('b'){ a }
       }
    }

    c = Class::new b

    assert{ c.a == 42 }
    assert{ c.a? }
    assert{ c.a = 'forty-two' }
    assert{ c.a == 'forty-two' }
    assert{ b.a == 42 }

    o = c::new

    assert{ o.a == 42 }
    assert{ o.a? }
    assert{ (o.a = nil) == nil }
    assert{ not o.a? }
  end
이렇게 해서 모든 koan의 테스트를 살펴봤다. 이번 퀴즈에서 깨달은 것은 어떤 인스턴스 변수에 디폴트 값을 줘야 하는 경우, 그 변수를 먼저 생성하고 거기에 값을 써넣을 생각을 할 것이 아니라 (이렇게 하면 변수가 개체 인스턴스 변수냐, 클래스 인스턴스 변수냐, 값을 대입하려는 지금 컨텍스트가 뭐냐 등등 복잡하게 따져줘야 한다) 어떤 상태에 있든 지금 컨텍스트에 해당 인스턴스 변수가 존재하느냐만 보고 없으면 디폴트 값만 돌려주도록 하면 된다는 것이다. 컬럼부스 달걀 같다. 아래는 모든 공안 (公案)을 통과하면 보이는 메시지이다. "산은 그저 산이로다" 사바하...
koan_1 has expanded your awareness
koan_2 has expanded your awareness
koan_3 has expanded your awareness
koan_4 has expanded your awareness
koan_5 has expanded your awareness
koan_6 has expanded your awareness
koan_7 has expanded your awareness
koan_8 has expanded your awareness
koan_9 has expanded your awareness
mountains are again merely mountains

덧글

  • passion 2006/02/21 03:09 # 답글

    크흑. 어렵군요. 저는 6까지 통과하고 항복해버렸어요...
    올려주신 솔루션 아주아주 아름답네요 ㅎㅎ
댓글 입력 영역