Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

RBS:새로운 Ruby 3 타이핑 언어 실행

대망의 Ruby 버전 3.0.0이 드디어 출시되었습니다. 이전 버전에 비해 3배 빠른 성능 향상, 동시성 병렬 실험 기능 등과 같은 많은 개선 사항과 함께 Ruby 팀은 Ruby의 동적 입력을 위한 새로운 구문 언어인 RBS도 도입했습니다.

Sorbet과 같은 정적 유형 검사를 위해 커뮤니티에서 개발한 도구의 성공을 기반으로 팀이 수년 동안 논의해 왔던 것입니다.

Sorbet은 Stripe가 지원하는 강력한 유형 검사기입니다. RBI 파일에 주석을 달거나 정의하여 코드를 확인합니다. RBI 파일은 차례로 정적 구성 요소와 동적 구성 요소 사이의 인터페이스로 작동하여 구성 요소에 대한 "설명"(상수, 조상, 메타프로그래밍 코드 등)을 제공합니다.

그렇다면 Sorbet은 대부분 정적 검사를 다루고 RBS는 동적 타이핑을 다루기 위해 만들어졌다면 이들 사이의 차이점은 무엇입니까? 둘은 어떻게 공존할 것인가? 언제 다른 것 대신에 하나를 사용해야 합니까?

RBS의 주요 역할에 대한 상당히 일반적인 질문입니다. 그것이 우리가 이 작품을 쓰기로 결정한 이유입니다. 실제로 능력에 따라 채택을 고려해야 하는 이유를 명확히 하기 위해. 바로 뛰어들자!

기본부터 시작하기

정적 입력 _및의 차이점에 대한 명확한 이해부터 시작하겠습니다. 동적 타이핑_. 기본적이지만 RBS의 역할을 이해하기 위해서는 반드시 파악해야 하는 핵심 개념입니다.

정적으로 유형이 지정된 언어의 코드 스니펫을 참조로 사용하겠습니다.

➜
String str = "";
str = 2.4;

그러한 언어가 객체와 변수의 유형에 관심을 갖는다는 것은 새삼스러운 일이 아닙니다. 즉, 위와 같은 코드는 오류를 발생시킵니다.

Ruby는 JavaScript, Python 및 Objective-C와 같은 다른 많은 언어와 마찬가지로 개체에 대해 어떤 유형을 대상으로 하는지에 대해 그다지 주의를 기울이지 않습니다.

아래와 같이 Ruby의 동일한 코드가 성공적으로 실행됩니다.

➜  irb
str = ""
str = 2.4
puts str # prints 2.4

이것은 Ruby의 인터프리터가 동적으로 한 유형에서 다른 유형으로 전환합니다.

그러나 통역사가 허용하는 것에는 한계가 있습니다. 예를 들어 다음 코드 변경을 사용하십시오.

➜  irb
val = "6.0"
result = val + 2.0
puts result

그러면 다음과 같은 오류가 발생합니다.

오류:Float를 String으로 암시적으로 변환하지 않음

예를 들어 JavaScript로 동일한 코드를 실행하면 제대로 실행됩니다.

이야기의 교훈:Ruby는 실제로 유형을 동적으로 추론합니다. 그러나 다른 주요 동적 언어와 달리 모든 것을 허용하지는 않습니다. 주의하세요!

바로 이 부분에서 유형 검사기(정적이든 동적이든)가 유용합니다.

RBS 대 소르베

맞습니다. 동적 대 정적 문제에 대한 요점을 알았습니다. 하지만 소르베는 어떻습니까? 더 이상 사용되지 않습니까?

전혀. RBS와 Sorbet의 주요(그리고 아마도 가장 중요한) 차이점은 전자는 언어일 뿐이고 후자는 완전한 유형 검사기라는 것입니다.

Ruby 팀은 구조를 설명하는 것이 RBS의 주요 목표라고 주장합니다. 당신의 코드의. 유형 검사를 수행하지 않고, 오히려 유형 검사기(Sorbet 또는 기타)가 유형 검사에 사용할 수 있는 구조를 정의합니다. 코드 구조는 새 파일 확장자(.rbs)에 저장됩니다. .

이를 확인하기 위해 다음 Ruby 클래스를 예로 들어 보겠습니다.

class Super
    def initialize(val)
      @val = val
    end
 
 
    def val?
      @val
    end
end
 
class Test < Super
  def initialize(val, flag)
    super(val)
    @flag = flag
  end
 
  def flag?
    @flag
  end
end

Ruby에서 간단한 상속을 나타냅니다. 여기서 주목해야 할 흥미로운 점은 flag를 제외하고 클래스에서 사용된 각 속성의 유형을 추측할 수 없다는 것입니다. .

flag 이후 기본값으로 초기화된 상태로 제공되므로 개발자와 유형 검사기 모두 더 이상 오용되지 않도록 유형을 유추할 수 있습니다.

다음은 위의 클래스를 RBS 형식으로 적절하게 표현한 것입니다.

class Super
  attr_reader val : untyped
 
  def initialize : (val: untyped) -> void
end
 
class Test < Super
  attr_reader flag : bool
 
  def initialize : (val: untyped, ?flag: bool) -> void
  def flag? : () -> bool
end

이것을 소화하는 데 시간을 할애하십시오. 선언 언어이므로 RBS 파일에는 서명만 나타날 수 있습니다. 간단하죠?

CLI 도구(나중에 자세히 설명) 또는 사용자에 의해 자동 생성되었는지 여부에 관계없이 유형에 untyped 주석을 추가하는 것이 더 안전합니다. 추측할 수 없을 때.

val 유형이 확실하다면 , 예를 들어 RBS 매핑을 다음과 같이 전환할 수 있습니다.

class Super
  attr_reader val : Integer
 
  def initialize : (val: Integer) -> void
end

Ruby와 Sorbet 팀 모두 RBS의 생성 및 개선을 위해 노력하고 있었다는 점에 주목하는 것도 중요합니다. Ruby 팀이 이 프로젝트에서 많은 것을 미세 조정하는 데 도움이 된 것은 Sorbet 팀의 수년간 유형 검사 경험이었습니다.

RBS와 RBI 파일 간의 상호 운용성은 아직 개발 중입니다. 목표는 Sorbet 및 기타 모든 검사기 도구가 공식적이고 중앙 집중식 기반을 따르는 것입니다.

RBS CLI 도구

Ruby 팀이 RBS를 개발할 때 고려했던 한 가지 중요한 고려 사항은 개발자가 사용해 보고 사용법을 배우는 데 도움이 되는 CLI 도구를 제공하는 것이었습니다. rbs라고 합니다. Ruby 3와 함께 기본적으로 제공됩니다. 아직 Ruby 버전을 업그레이드하지 않았다면 gem을 프로젝트에 직접 추가할 수도 있습니다.

➜  gem install rbs

rbs help 명령 사용 가능한 명령과 함께 명령 사용법이 표시됩니다.

사용 가능한 명령 목록

이러한 명령의 대부분은 Ruby 코드 구조를 구문 분석하고 분석하는 데 중점을 둡니다. 예를 들어 ancestors 명령은 주어진 클래스의 계층 구조를 스윕하여 해당 클래스의 조상을 확인합니다.

➜  rbs ancestors ::String
::String
::Comparable
::Object
::Kernel
::BasicObject

methods 명령 주어진 클래스의 모든 메소드 구조를 표시합니다:

➜  rbs methods ::String
! (public)
!= (public)
!~ (public)
...
Array (private)
Complex (private)
Float (private)
...
autoload? (private)
b (public)
between? (public)
...

특정 메소드 구조를 보고 싶으십니까? methods로 이동 :

➜  rbs method ::String split
::String#split
  defined_in: ::String
  implementation: ::String
  accessibility: public
  types:
      (?::Regexp | ::string pattern, ?::int limit) -> ::Array[::String]
    | (?::Regexp | ::string pattern, ?::int limit) { (::String) -> void } -> self

오늘 RBS를 시작하는 사람들을 위해 prototype 명령 이미 존재하는 클래스의 스캐폴딩 유형에 많은 도움이 될 수 있습니다. 이 명령은 RBS 파일의 프로토타입을 생성합니다.

이전 Test < Super를 살펴보겠습니다. 상속 예제 및 코드를 appsignal.rb 파일에 저장 . 그런 다음 다음 명령을 실행합니다.

➜  rbs prototype rb appsignal.rb

명령이 rb를 허용하기 때문에 , rbi런타임 생성기를 사용하려면 prototype 바로 뒤에 스캐폴딩할 특정 유형의 파일을 제공해야 합니다. 명령 다음에 파일 경로 이름이 옵니다.

실행 결과는 다음과 같습니다.

class Super
  def initialize: (untyped val) -> untyped
 
  def val?: () -> untyped
end
 
class Test < Super
  def initialize: (untyped val, ?flag: bool flag) -> untyped
 
  def flag?: () -> untyped
end

첫 번째 RBS 버전과 매우 유사합니다. 앞서 언급했듯이 도구는 untyped로 표시합니다. 추측할 수 없는 모든 유형.

메서드 반환에도 포함됩니다. flag의 반환 유형을 확인하세요. 정의. 개발자는 메서드가 항상 부울을 반환한다고 확신할 수 있지만 Ruby의 동적 특성으로 인해 도구가 그렇다고 100% 말할 수는 없습니다.

그리고 그 때 또 다른 Ruby 3의 자식인 TypeProf가 구출됩니다.

TypeProf 도구

TypeProf는 일부 구문 트리 해석 위에 만들어진 Ruby용 유형 분석 도구입니다.

아직 실험적이지만 코드가 수행하려는 작업을 이해하는 데 있어 매우 강력한 것으로 입증되었습니다.

아직 Ruby 3가 없다면 프로젝트에 gem을 추가하기만 하면 됩니다.

➜  gem install typeprof

이제 동일한 appsignal.rb를 실행해 보겠습니다. 이에 반대하는 파일:

➜  typeprof appsignal.rb

이것은 출력입니다:

# Classes
class Super
  @val: untyped
  def initialize: (untyped) -> untyped
  def val?: -> untyped
end
 
class Test < Super
  @val: untyped
  @flag: true
 
  def initialize: (untyped, ?flag: true) -> true
  def flag?: -> true
end

flag 지금 매핑되어 있습니다. 이것은 RBS 프로토타입이 하는 것과 달리 TypeProf가 특정 변수에 대해 수행되는 작업을 이해하기 위해 메서드 본문을 스캔하기 때문에 가능합니다. 이 변수에 대한 직접적인 변경을 식별할 수 없었기 때문에 TypeProf는 메서드 반환을 부울로 안전하게 매핑했습니다.

예를 들어 TypeProf는 Test를 인스턴스화하고 사용하는 다른 클래스에 액세스할 수 있습니다. 수업. 이를 통해 코드에 더 깊이 들어가 예측을 미세 조정할 수 있습니다. appsignal.rb 끝에 다음 코드 스니펫이 추가되었다고 가정해 보겠습니다. 파일:

testSub = Test.new("My value", "My value" == "")
testSup = Super.new("My value")

그리고 initialize 다음에 대한 메서드 서명:

def initialize(val, flag)

명령을 다시 실행하면 다음과 같이 출력되어야 합니다.

# Classes
class Super
  @val: String
 
  def initialize: (String) -> String
  def val?: -> String
end
 
class Test < Super
  @val: String
  @flag: bool
 
  def initialize: (String val, bool flag) -> bool
  def flag?: -> bool
end

정말 멋지네요!

TypeProf는 상속된 속성을 잘 다룰 수 없습니다. 이것이 우리가 새로운 Super를 인스턴스화하는 이유입니다. 물체. 그렇지 않으면 해당 val을 얻지 못할 것입니다. String입니다. .

TypeProf의 주요 장점은 안전성입니다. 확실히 뭔가를 알아낼 수 없을 때마다 untyped 반환됩니다.

부분 RBS 사양

공식 문서의 중요한 경고 중 하나는 TypeProf가 매우 강력하지만 RBS 코드 측면에서 생성할 수 있는 것과 생성할 수 없는 것에 대한 제한 사항을 알고 있어야 한다는 것입니다.

예를 들어 Ruby 개발자들 사이에서 일반적인 관행은 인수에 따라 메서드의 다른 동작을 호출하는 메서드 오버로딩입니다.

새로운 메소드 spell Super에 추가됩니다. Integer를 반환하는 클래스 또는 String 매개변수 유형에 따라:

def spell(val)
  if val.is_a?(String)
    ""
  else
    0
  end
end

RBS는 공용체 유형(여러 가능한 유형을 나타내는 값) 구문을 통해 오버로드를 처리할 수 있도록 하여 이 방식을 수용합니다.

def spell: (String) -> String | (Integer) -> Integer

TypeProf는 메서드의 본문을 분석하는 것만으로는 이것을 유추할 수 없습니다. 이를 돕기 위해 RBS 파일에 이러한 정의를 수동으로 추가할 수 있으며 TypeProf는 항상 먼저 지침을 확인합니다.

이를 위해 명령 끝에 RBS 파일 경로를 추가해야 합니다.

typeprof appsignal.rb appsignal.rbs

아래에서 새 출력을 참조하세요.

class Super
  ...
  def spell: (untyped val) -> (Integer | String)
end

또한 Kernel#p를 통해 런타임 중에 실제 유형을 확인할 수도 있습니다. appsignal.rb 끝에 다음 두 줄을 추가하여 오버로딩이 작동하는지 테스트합니다. 파일:

p testSup.spell(42)
p testSup.spell("str")

다음과 같이 출력해야 합니다.

# Revealed types
#  appsignal.rb:11 #=> Integer
#  appsignal.rb:12 #=> String
 
...

자세한 내용은 공식 문서, 특히 TypeProf 제한 사항에 관한 섹션을 참조하십시오.

오리 타이핑

당신은 전에 그것에 대해 들어본 적이 있습니다. Ruby 개체가 오리가 하는 모든 작업을 수행하면 오리입니다.

우리가 보았듯이 Ruby는 당신의 객체가 무엇을 의미하는지 상관하지 않습니다. 유형은 개체 참조뿐만 아니라 동적으로 변경될 수 있습니다.

도움이 되기는 하지만 오리 타이핑은 까다로울 수 있습니다. 예를 들어 보겠습니다.

지금부터 val Super에 대해 선언한 속성 String인 클래스 , 항상 정수로 변환할 수 있어야 합니다.

개발자가 항상 변환을 보장할 것이라고 믿는 대신(그렇지 않으면 오류가 발생할 수 있음) 인터페이스를 만들 수 있습니다.

interface _IntegerConvertible
   def to_int: () -> Integer
end

인터페이스 유형은 구체적인 클래스 및 모듈에서 분리된 하나 이상의 메소드를 제공합니다. 이렇게 하면 특정 유형이 Super 인스턴스화에 전달되기를 원할 때 간단히 다음을 수행할 수 있습니다.

class Super
  attr_reader val : _IntegerConvertible
 
  def initialize : (val: _IntegerConvertible) -> void
end

이 인터페이스를 구현하는 구체적인 클래스 또는 모듈은 적절한 유효성 검사가 수행되었는지 확인해야 합니다.

메타프로그래밍

아마도 Ruby의 가장 동적인 기능 중 하나는 런타임 중에 스스로 코드를 생성하는 코드를 생성하는 기능일 것입니다. 그것이 메타프로그래밍입니다.

사물의 불확실한 특성 때문에 RBS CLI 도구는 메타프로그래밍 코드에서 RBS를 생성할 수 없습니다.

다음 스니펫을 예로 들어 보겠습니다.

class Test
    define_method :multiply do |*args|
        args.inject(1, :*)
    end
end
 
p Test.new.multiply(2, 3, 5)

이 클래스는 multiply라는 메서드를 정의합니다. 런타임에 인수를 삽입하고 각각에 이전 결과를 곱하도록 지시합니다.

RBS prototype을 실행하면 명령, 이것은 출력이어야 합니다:

class Test
end

메타프로그래밍 코드의 복잡성에 따라 TypeProf는 여전히 코드에서 무언가를 추출하기 위해 최선을 다할 것입니다. 하지만 항상 보장되는 것은 아닙니다.

항상 RBS 파일에 고유한 유형 매핑을 추가할 수 있으며 TypeProf는 미리 이를 따릅니다. 이는 메타프로그래밍에도 유효합니다.

팀이 메타프로그래밍에 대한 업데이트를 포함할 수 있는 새로운 기능을 지속적으로 출시하고 있기 때문에 최신 저장소 변경 사항을 계속 업데이트하는 것도 중요합니다.

즉, 코드베이스에 일부 유형의 메타프로그래밍이 포함되어 있는 경우 이러한 도구에 주의하십시오. 맹목적으로 사용하지 마세요!

마무리

지금까지 논의한 내용과 RBS 및 TypeProf 모두에 대한 에지 사용 사례에 대한 자세한 내용이 있습니다.

따라서 이에 대한 자세한 내용은 공식 문서를 참조하세요.

RBS는 여전히 신선하지만 다른 도구로 코드베이스를 유형 검사하는 데 사용되는 Rubyists에 이미 큰 영향을 미쳤습니다.

당신은 어때요? 사용해 보셨나요? RBS에 대해 어떻게 생각하십니까?

추신 Ruby Magic 게시물이 언론에 공개되는 즉시 읽고 싶다면 Ruby Magic 뉴스레터를 구독하고 게시물을 놓치지 마세요!