모듈
모듈 (Modules)
모듈은 두 가지 목적으로 쓰여요.
- 다른 타입·메서드·상수를 정의하기 위한 네임스페이스
- 다른 타입에 섞어 넣을 수 있는 부분 타입(partial type)
네임스페이스로서의 모듈 예시를 볼게요.
module Curses
class Window
end
end
Curses::Window.new
라이브러리 작성자는 이름 충돌을 피하기 위해 정의를 모듈 안에 넣을 것을 권장해요. 표준 라이브러리는 타입과 메서드가 아주 흔해서 긴 이름을 쓰지 않으려고 보통 네임스페이스를 두지 않아요.
출처: Crystal 공식 문서
본문
부분 타입으로 모듈을 사용하려면 include나 extend를 써요.
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
include와 extend 둘 다, 모듈에 정의된 상수를 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
모듈 타입 검사
모듈은 타입 검사에도 사용할 수 있어요.
이름이 A와 B인 두 모듈을 정의해 볼게요.
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로 클래스 메서드를 만드는 방법