크리스마스의 전통대로 올해도 루비
의 새 버전이 릴리즈되었습니다. 향상된 성능, 매력적인 신기능이 추가되었는데요, 그중에서도 너무나도 반가운 소식인 타입
이 눈길을 사로잡습니다.
루비를 사용하며 가장 아쉬웠던게 타입이 없다는 점이었는데, 이번 업데이트를 통해 그 갈증이 많이 해소되지 않을까 싶습니다.
루비의 타입은 RBS
를 통해 기술한다고 합니다. README 에 보면
RBS is a language to describe the structure of Ruby programs. You can write down the definition of a class or module: methods defined in the class, instance variables and their types, and inheritance/mix-in relations. It also allows declaring constants and global variables.
라고 되어있네요.
간단히 해석해보자면,
TS <-> JS
의 관계랑은 조금 다른듯 합니다. TS 는 JS 에 타입을 추가한 새로운 언어인 반면, RBS 는 Ruby 파일은 그대로 두고, 타입의 정의만을 기술하는 RBS 파일을 추가해서 사용하네요.
언어에 타입이 추가되었다기보다는 타입 명세를 정의하고, 실행 전 타입을 미리 검사해서 문제를 막겠다! 에 가까운 늬앙스인것 같습니다.
그럼 간단하게 어떻게 사용하는지 알아보겠습니다.
기본적으로 TS 같은 타이핑 방식을 사용합니다.
module ChatApp
VERSION: String
# 타입스크립트처럼 :타입 으로 타입을 나타낼 수 있음.
class User
attr_reader login: String
attr_reader email: String
# (인자) -> 리턴타입 으로 함수타입을 나타낼 수 있음.
# 아래처럼 (인자명: 타입) -> 리턴타입 이면 ruby 에서 넘길때도 login:"hi"
# 이렇게 넘겨줘야 함.
def initialize: (login: String, email: String) -> void
end
class Message
attr_reader from: User | Bot # | 을 사용하여 SUM 타입 정의가능
attr_reader reply_to: Message? # `?` 은 옵셔널 타입. Nil 이어도 된다.
def initialize: (from: User | Bot, string: String) -> void
def reply: (from: User | Bot, string: String) -> Message
end
class Channel
attr_reader name: String
attr_reader messages: Array[Message]
attr_reader users: Array[User]
attr_reader bots: Array[Bot]
def initialize: (name: String) -> void
# `{` `}` 는 블럭을 의미함.
# 메소드 오버로딩 가능.
# [타입1,타입2] 으로 제네릭처럼 사용가능.
def each_member: () { (User | Bot) -> void } -> void
| () -> Enumerator[User | Bot, void]
end
end
전반적으로 TS 의 타입을 사용하셨다면, 무리없이 이해할 수 있는 방식입니다.
한번 사용해보려고 하니, 대체 어떻게 사용할 수 있는지
자세히 나와있지 않았습니다.
뭔가 TypeChecking 을 하는 커맨드가 있다던가... TS 처럼 빌드를 해야한다거나 공식으로 제공하는 방법이 있을것 같은데, 공식문서에는 따로 소개되어있지 않습니다.
그래서 일단은 steep
이라는 Gem 을 사용해서 돌려보도록 하겠습니다. 혹시 공식 방법을 아시는 분이 있다면 덧글로 알려주시면 감사하겠습니다.
(https://github.com/soutaro/steep)
gem install steep
steep init
위 명령어로 SteepFile 이 생성됩니다. 적절히 수정해봅시다.
target :app do
check "testApp" # rb 파일들 경로
signature "sig" # rbs 파일들의 경로
library "set", "pathname"
end
testApp 디렉토리를 만들어서 rb 파일들을 넣어주고, sig 디렉토리를 만들어서 rbs 파일들 넣어줍니다.
최종 파일 상태.
├── Gemfile ├── Gemfile.lock ├── Steepfile ├── sig │ └── rbs파일이름.rbs └── testApp └── ruby파일이름.rb
위의 예제는 조금 복잡하니, 간단한 helloWorld 클래스를 만들어 직접 돌려보도록 합시다.
# Practic.rbs
class Practice
@print_str: String
def initialize: (String) -> untyped
def print: -> String
end
루비파일도 작성해봅니다.
# Practic.rb
class Practice
def initialize(str)
@print_str = str
end
def print
return "#{@print_str} world!"
end
end
이제 저 클래스를 만들고 돌려봅시다.
# run.rb
practice = Practice.new("hello")
p practice.print
teihong@Teiui-MacBookPro: $ steep check
teihong@Teiui-MacBookPro: $
아무 오류도 뜨지 않았습니다. 그럼 한번 잘못된 타입을 넣어볼까요?
# run.rb
pr1 = Practice.new(12345)
p pr1.print
teihong@Teiui-MacBookPro:$ steep check
testApp/run.rb:1:26: ArgumentTypeMismatch: receiver=singleton(::Practice), expected=::String, actual=::Integer (12345)
오.. String 타입을 예상했는데, Integer 가 날라왔다고 합니다.
그럼 한번 SUM TYPE 으로 인자를 정의해서 String 혹은 Integer 를 받도록 해볼까요?
# Practic.rbs 수정
class Practice
@print_str: String | Integer
def initialize: (String|Integer) -> untyped
def print: -> String
end
teihong@Teiui-MacBookPro$ steep check
teihong@Teiui-MacBookPro$
이제 Interger 타입도 잘 받는것을 확인할 수 있네요.
rbs 의 문법은 여기에 정리되어있습니다.
(https://github.com/ruby/rbs/blob/master/docs/syntax.md)
현대의 타입언어에서 지원하는 대부분의 기능들을 지원해주는 것을 확인할 수 있습니다.
루비 3.0 에서는 타입 말고도 동시성을 위한 Ractor
라는 기능도 추가되었는데요,
좀더 기술이 성숙되면 Ractor
에 대해서도 한번 알아보면 좋을것 같네요.
ruby 3.0
릴리즈 기념으로 rbs
에 대해 알아보았습니다.
사실 TS 처럼 TypedRuby 가 나왔다고 생각했는데, 조금의 방향성 차이가 있는것 같습니다.
타입체킹을 한다는 점에서는 환영할만 하지만, 따로 파일을 분리해야한다는 점이 저에겐 다소 불편하게 느껴지기도 합니다.
특히 TS 의 경우 TS 가 엄청 똑똑해서, 개발시점에 타입추론을 잘 해주고 자동완성을 비롯한 편의적 기능을 많이 지원해주는데 RBS 는 현재로서는 개발 편의적인 기능들의 지원이 없는것 같네요.
또 아직은 자료도 많지 않아 루비의 타입이 프로덕션 환경에서 정착되기에는 조금 더 시간이 걸리지 않을까 하는 의견입니다.
하지만 루비에 타입이 들어온다는 것 자체가 너무나도 반가운 일이고 환영할만한 일입니다.
(이참에 루비를 쓰는 사람들이 많아지면 더 좋긴 하겠습니다... 애증의 언어 루비)
그럼 다음 글로 찾아뵙도록 하겠습니다.
잘 보고 갑니당 :)