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

Ruby에서 유형 검사 — 자신을 망가뜨리기 전에 스스로를 확인하십시오

재미있는 추측 게임으로 이 게시물을 시작하겠습니다. Ruby 애플리케이션에서 AppSignal이 추적하는 가장 일반적인 오류는 무엇이라고 생각하시나요?

많은 분들이 이 질문에 NoMethodError로 답했다고 가정하는 것이 좋습니다. , 개체에서 존재하지 않는 메서드를 호출하여 발생하는 예외입니다. 때때로 이것은 메소드 이름의 오타로 인해 발생할 수 있지만 더 자주는 예기치 않은 nil이 발생하는 잘못된 유형의 개체에서 메소드를 호출한 결과입니다. . 이러한 오류의 빈도를 줄이기 위해 Ruby 개발자로서 할 수 있는 일이 있습니까?

구조 대상은 무엇입니까?

텍스트 편집기나 프로그래밍 언어의 선택을 제외하고 유형 시스템에 대한 논의보다 더 빨리 열띤 토론을 벌일 수 있는 주제는 거의 없습니다. 여기에서 자세히 설명할 시간은 없지만 Chris Smith의 "유형 시스템에 대해 토론하기 전에 알아야 할 사항"은 이에 대해 훌륭하게 설명합니다.

가장 넓은 의미에서 유형 시스템은 정적 및 동적의 두 가지 주요 범주로 나눌 수 있습니다. 전자는 컴파일러나 별도의 도구를 통해 미리 발생하지만 런타임 중에 동적 유형 검사가 발생하므로 실제 유형이 개발자의 기대와 일치하지 않는 경우 예외가 발생할 수 있습니다.

두 철학의 지지자들은 강력한 의견을 가지고 있지만, 슬프게도 많은 오해가 떠돌고 있습니다. 정적 유형 지정에는 많은 유형 주석이 필요하지 않습니다. 많은 현대 컴파일러는 "유형 추론"으로 알려진 프로세스를 자체적으로 유형을 파악할 수 있습니다. 반면에, 동적으로 유형이 지정된 언어는 정적으로 유형이 지정된 언어보다 훨씬 더 높은 결함률을 나타내지 않는 것 같습니다.

오리 타이핑

Ruby 자체는 동적으로 유형이 검사되는 언어이며 "덕 타이핑" 접근 방식을 따릅니다.

<블록 인용>

오리처럼 걷고 오리처럼 꽥꽥 거리면 오리임에 틀림없습니다.

이것이 의미하는 바는 Ruby 개발자는 일반적으로 객체의 유형에 대해 너무 많이 걱정하지 않고 특정 "메시지"(또는 메소드)에 응답하는지 여부에 대해 걱정한다는 것입니다.

그렇다면 Ruby에서 정적 타이핑에 신경을 쓰는 이유는 무엇입니까? 코드를 마술처럼 버그 없이 만드는 만병통치약은 아니지만 다음과 같은 이점이 있습니다.

  • 정확성:정적 입력은 특정 클래스를 방지하는 데 효과적입니다. 앞서 언급한 NoMethodError와 같은 버그 .
  • 도구:종종 개발 중에 사용 가능한 정적 유형 정보가 있으면 도구 옵션이 향상됩니다(예:IDE의 리팩토링 지원 등).
  • 문서화:많은 정적으로 유형이 지정된 언어에는 뛰어난 문서화 도구가 내장되어 있습니다. Haskell의 Hoogle은 유형 서명으로 함수를 조회할 수 있는 검색 엔진을 제공하여 이를 매우 효과적으로 사용합니다.
  • 성능:컴파일러에서 사용할 수 있는 정보가 많을수록 잠재적으로 적용할 수 있는 성능 최적화가 더 많아집니다.

이 목록은 완전하지 않으며 이러한 대부분의 사항에 대한 반례를 찾을 수 있지만 여기에는 분명히 핵심이 있습니다.

점진적 유형 검사

최근 몇 년 동안 일반적으로 "점진적 유형 검사"라고 하는 접근 방식이 JS용 TypeScript에서 PHP용 Hack 및 Python용 mypy에 이르기까지 다양한 동적으로 유형 검사된 언어에 침투했습니다. 이러한 접근 방식의 공통점은 전부 아니면 전무 접근 방식이 필요하지 않지만 대신 개발자가 적절하다고 생각하는 대로 변수 및 표현식에 유형 정보를 점진적으로 추가할 수 있다는 것입니다. 이것은 시스템의 가장 중요한 부분을 정적으로 검사하면서 나머지는 유형화하지 않고 런타임에 검사할 수 있는 기존의 대규모 코드베이스에 특히 유용합니다. 이 기사의 나머지 부분에서 살펴볼 Ruby의 모든 유형 검사 솔루션은 동일한 접근 방식을 따릅니다.

옵션

Ruby 개발자가 개발 워크플로에 정적 유형 검사를 추가하려는 이유를 살펴본 후 현재 인기 있는 몇 가지 옵션을 살펴볼 차례입니다. 그러나 Ruby에 정적 유형 검사를 추가하는 아이디어는 새로운 것이 아닙니다. 메릴랜드 대학의 연구원들은 이미 2009년에 Diamondback Ruby(Druby)라는 Ruby 확장에 대해 작업했으며 Tufts 대학 프로그래밍 언어 그룹은 2013년에 The Ruby Type Checker라는 논문을 발표했으며, 이는 결국 유형을 제공하는 RDL 프로젝트로 이어졌습니다. 라이브러리로서의 기능을 확인하고 계약에 따라 설계합니다.

샤베트

Stripe에서 개발한 Sorbet은 현재 Ruby에 대해 가장 많이 언급되는 유형 검사 솔루션입니다. 특히 Shopify, GitLab, Kickstarter 및 Coinbase와 같은 대기업이 클로즈 베타 단계에서 얼리 어답터였기 때문입니다. 작년 Ruby Kaigi에서 처음 발표되었으며 올해 6월 20일에 첫 공개되었습니다. Sorbet은 최신 C++로 작성되었으며 Matz의 기본 설정(인용:"나는 유형 주석을 싫어합니다")에도 불구하고 유형 주석에 기반한 접근 방식을 선택했습니다. Sorbet에서 특히 흥미로운 점 중 하나는 Ruby의 극도로 동적인 특성과 메타프로그래밍 기능이 정적 유형 시스템에서 어렵기 때문에 정적 및 동적 유형 검사의 조합을 선택한다는 것입니다.

# typed: true
class Test
  extend T::Sig
 
  sig {params(x: Integer).returns(String)}
  def to_s(x)
    x.to_s
  end
end

유형 검사를 활성화하려면 먼저 # typed: true를 추가해야 합니다. 마법의 주석을 추가하고 T::Sig로 클래스를 확장하세요. 기준 치수. 실제 유형 주석은 sig로 지정됩니다. 방법:

sig {params(x: Integer).returns(String)}

이 메서드가 x라는 단일 인수를 사용하도록 지정합니다. Integer 유형입니다. String을 반환합니다. . 잘못된 인수 유형으로 이 메서드를 호출하려고 하면 오류가 발생합니다.

Test.new.to_s("42")
# Expected Integer but found String("42") for argument x

이러한 기본 검사 외에도 Sorbet에는 소매에 몇 가지 트릭이 더 있습니다. 예를 들어, 두려운 NoMethodError로부터 우리를 구할 수 있습니다. nil에서 :

users = T::Array[User].new
user = users.first
user.username
 
# Method username does not exist on NilClass component of T.nilable(User)

위의 스니펫은 User의 빈 배열을 정의합니다. 객체 및 첫 번째 요소에 액세스하려고 할 때(nil 반환) ) Sorbet은 username이라는 이름의 메서드가 없음을 올바르게 경고합니다. NilClass에서 사용할 수 있습니다. . 그러나 특정 값이 nil이 될 수 없다고 확신하는 경우 , 우리는 T.must를 사용할 수 있습니다. Sorbet에 이 사실을 알리기 위해:

users = T::Array[User].new
user = T.must(users.first)
user.username

위의 코드는 이제 형식 검사를 수행하지만 런타임 예외가 발생할 수 있으므로 이 기능을 주의해서 사용하십시오.

Sorbet이 우리를 위해 할 수 있는 일이 훨씬 더 많습니다:데드 코드 감지, 유형 고정(기본적으로 변수를 특정 유형으로 커밋, 예를 들어 문자열이 할당되면 정수를 할당할 수 없음) 또는 기능 인터페이스를 정의합니다.

또한 Sorbet은 "Ruby Interface" 파일(rbi ) sorbet/에 보관 현재 작업 디렉토리의 폴더. 이를 통해 프로젝트에서 사용하는 모든 gem에 대한 인터페이스 정의를 생성할 수 있으므로 더 많은 유형 오류를 찾는 데 도움이 됩니다.

Sorbet에는 단일 기사에서 다룰 수 있는 것보다 훨씬 더 많은 것이 있지만(예:다양한 엄격성 수준 또는 메타프로그래밍 플러그인), 해당 문서는 이미 꽤 훌륭하고 PR을 위해 공개되어 있습니다.

가파름

Sorbet의 가장 널리 알려진 대안은 Soutaro Matsumoto의 Steep입니다. 주석을 사용하지 않으며 자체적으로 유형 유추를 수행하지 않습니다. 대신 .rbi에 완전히 의존합니다. sig의 파일 디렉토리.

다음의 간단한 Ruby 클래스부터 시작해 보겠습니다.

class User
  attr_reader :first_name, :last_name, :address
 
  def initialize(first_name, last_name, address)
    @first_name = first_name
    @last_name = last_name
    @address = address
  end
 
  def full_name
    "#{first_name} #{last_name}"
  end
end

이제 초기 user.rbi를 스캐폴드할 수 있습니다. 다음 명령으로 파일:

$ steep scaffold user.rb > sig/user.rbi

그 결과 시작점으로 사용되는 다음 파일이 생성됩니다(모든 유형이 any로 지정되었다는 사실로 설명됨). , 안전을 제공하지 않음):

class User
  @first_name: any
  @last_name: any
  @address: any
  def initialize: (any, any, any) -> any
  def full_name: () -> String
end

그러나 이 시점에서 check를 입력하려고 하면 몇 가지 오류가 발생합니다.

$ steep check
user.rb:11:7: NoMethodError: type=::User, method=first_name (first_name)
user.rb:11:21: NoMethodError: type=::User, method=last_name (last_name)

우리가 이것을 보는 이유는 Steep이 attr_reader를 통해 정의된 메소드를 알기 위해 특별한 주석이 필요하기 때문입니다. s, 그래서 그것을 추가합시다:

# @dynamic first_name, last_name, address
attr_reader :first_name, :last_name, :address

또한 생성된 .rbi에 메서드에 대한 정의를 추가해야 합니다. 파일. 그 동안 any의 서명도 변경해 보겠습니다. 실제 유형:

class User
  @first_name: String
  @last_name: String
  @address: Address
  def initialize: (String, String, Address) -> any
  def first_name: () -> String
  def last_name: () -> String
  def address: () -> Address
  def full_name: () -> String
end

이제 모든 것이 예상대로 작동하고 steep check 오류를 반환하지 않습니다.

지금까지 본 것 외에도 Steep은 제네릭(예:Hash<Symbol, String> ) 및 공용체 유형으로, 여러 유형 중 하나 또는 선택을 나타냅니다. 예를 들어, 사용자의 top_post 메소드는 사용자가 작성한 가장 높은 순위의 게시물을 반환하거나 nil 그들이 아직 아무것도 기여하지 않은 경우. 이것은 유니온 유형 (Post | nil)을 통해 표현됩니다. , 해당 서명은 다음과 같습니다.

def top_post: () -> (Post | nil)

Steep은 확실히 Sorbet보다 기능이 적지만 여전히 유용한 도구이며 Matz가 구상한 Ruby 3의 유형 검사와 더 일치하는 것 같습니다.

루비 유형 프로파일러

Cookpad의 Yusuke Endoh(Ruby 개발자 서클에서 "mame"으로 더 잘 알려짐)는 Ruby Type Profiler라는 소위 레벨 1 유형 검사기를 개발하고 있습니다. 여기에 제시된 다른 솔루션과 달리 서명 파일이나 유형 주석이 필요하지 않지만 대신 구문 분석하는 동안 Ruby 프로그램에 대해 가능한 한 많이 추론하려고 합니다. Steep이나 Sorbet보다 잠재적인 문제를 훨씬 덜 포착하지만 개발자에게 추가 비용이 들지 않습니다.

요약

누구도 미래를 예측할 수는 없지만 Ruby의 유형 검사는 앞으로도 계속될 것 같습니다. 현재 .rbi에서 사용하기 위해 "Ruby 서명 언어"에 대한 표준화 작업이 진행 중입니다. 파일(Ruby Type Profiler에 의해 스캐폴딩될 수 있음)이므로 개발자는 원하는 도구를 사용할 수 있습니다. Steep은 이미 라이브러리 작성자가 보석과 함께 유형 정보를 제공할 수 있도록 허용하고 Sorbet은 TypeScript 정의를 위한 SimplyTyped 저장소에서 영감을 받은 셔벗 유형의 형태로 유사한 메커니즘을 가지고 있습니다. Ruby에서 유형 검사의 미래를 형성하는 데 관심이 있다면 지금이 바로 참여할 수 있는 좋은 기회입니다!