모듈

모듈 (Modules)

모듈은 두 가지 목적으로 쓰여요.

  • 다른 타입·메서드·상수를 정의하기 위한 네임스페이스
  • 다른 타입에 섞어 넣을 수 있는 부분 타입(partial type)

네임스페이스로서의 모듈 예시를 볼게요.

module Curses
  class Window
  end
end

Curses::Window.new

라이브러리 작성자는 이름 충돌을 피하기 위해 정의를 모듈 안에 넣을 것을 권장해요. 표준 라이브러리는 타입과 메서드가 아주 흔해서 긴 이름을 쓰지 않으려고 보통 네임스페이스를 두지 않아요.

출처: Crystal 공식 문서

본문

부분 타입으로 모듈을 사용하려면 includeextend를 써요.

include는 그 모듈에 정의된 메서드를 인스턴스 메서드로 포함하게 해요.

module ItemsSize
  def size
    items.size
  end
end

class Items
  include ItemsSize

  def items
    [1, 2, 3]
  end
end

items = Items.new
items.size # => 3

위 예시에서 마치 모듈의 size 메서드를 Items 클래스에 붙여 넣은 것처럼 동작해요. 실제 원리는 각 타입이 조상(ancestors) 또는 부모 목록을 갖는 것이에요. 기본적으로 이 목록은 슈퍼클래스로 시작하는데, 모듈이 include될 때마다 그 목록의 앞에 추가(prepend) 돼요. 메서드가 타입에서 발견되지 않으면 이 목록에서 찾아요. super를 호출하면 이 조상 목록의 첫 번째 타입이 사용돼요.

module은 다른 모듈을 include할 수 있어서, 메서드가 발견되지 않으면 include된 모듈들에서도 찾아봐요.

extend는 그 모듈에 정의된 메서드를 클래스 메서드로 포함하게 해요.

module SomeSize
  def size
    3
  end
end

class Items
  extend SomeSize
end

Items.size # => 3

includeextend 둘 다, 모듈에 정의된 상수를 include/extend하는 타입에서 사용할 수 있게 해요.

둘 다 최상위 레벨에서 사용할 수 있어서, 네임스페이스를 반복해서 쓰지 않을 수 있어요(이름 충돌 가능성은 높아지지만).

module SomeModule
  class SomeType
  end

  def some_method
    1
  end
end

include SomeModule

SomeType.new # OK, same as SomeModule::SomeType
some_method  # OK, 1

extend self

모듈에서 흔한 패턴은 extend self예요.

module Base64
  extend self

  def encode64(string)
    # ...
  end

  def decode64(string)
    # ...
  end
end

이렇게 하면 모듈을 네임스페이스로 사용할 수 있어요.

Base64.encode64 "hello" # => "aGVsbG8="

동시에 프로그램에 include하면 네임스페이스 없이도 메서드를 호출할 수 있어요.

include Base64

encode64 "hello" # => "aGVsbG8="

이 패턴이 유용하려면 메서드 이름이 모듈을 떠올릴 만한 것이어야 해요. 그렇지 않으면 이름 충돌 가능성이 높아지죠.

모듈은 인스턴스화할 수 없어요.

module Moo
end

Moo.new # undefined method 'new' for Moo:Module

모듈 타입 검사

모듈은 타입 검사에도 사용할 수 있어요.

이름이 AB인 두 모듈을 정의해 볼게요.

module A; end

module B; end

이것들을 클래스에 include할 수 있어요.

class One
  include A
end

class Two
  include B
end

class Three < One
  include B
end

그러면 이런 클래스들의 인스턴스를 클래스뿐 아니라 include된 모듈 기준으로도 타입 검사할 수 있어요.

one = One.new
typeof(one)  # => One
one.is_a?(A) # => true
one.is_a?(B) # => false

three = Three.new
typeof(three)  # => Three
three.is_a?(A) # => true
three.is_a?(B) # => true

이 덕분에 클래스 대신 모듈 타입을 기준으로 배열과 메서드를 정의할 수 있어요.

one = One.new
two = Two.new
three = Three.new

new_array = Array(A).new
new_array << one   # Ok, One includes module A
new_array << three # Ok, Three inherits module A

new_array << two # Error, because Two neither inherits nor includes module A

더 알아보기

  • virtual and abstract types: 추상 메서드와 모듈의 조합
  • class methods: extend로 클래스 메서드를 만드는 방법