O introducere în anotările statice de tip în Ruby

  • 5 minute de citit
  • 13 exemple de cod în Shell, Ruby și Python

Disponibil și în English

Pe această pagină

Introducere

Salut tuturor!

Am început anul acesta reînvățând limbajul de programare Ruby. Ruby este un limbaj frumos și elegant, și a fost unul dintre primele trei limbaje pe care le-am învățat cândva, pe CodeCademy.

În ianuarie am terminat de citit Well-Grounded Rubyist, Fourth Edition care mi-a oferit un început bun în Ruby. Nu sunt deloc priceput încă, dar am cumpărat și mai multe cărți despre Ruby pe care sper să le termin de citit anul acesta.

Motivul pentru care învăț un limbaj nou în fiecare an este că mi se pare plăcut să încerc lucruri noi și să citesc cărți. În ultimii trei ani am citit despre Rust, Kotlin și Dart și am făcut câteva proiecte secundare în ele.

Pe vremea când învățam Python nu existau tipuri, totul era neutru, iar era greu să îți dai seama ce face o metodă doar uitându-te la semnătura ei. Din fericire, PEP 484 a adăugat indicii de tip, iar am putut scrie cod ca acesta:

def greeting(name: str) -> str:    return 'Hello ' + name

Ruby are și el suport pentru tipuri, iar motivul pentru care scriu acest articol este să îți arăt cum să adaugi tipuri în proiectul tău Ruby folosind Sorbet și RBS.

Cele două abordări sunt diferite, să le explorăm!

Tipuri cu RBS

RBS, adică Ruby Signature, a fost dezvoltat de 7 ani și funcționează prin existența unui fișier .rbs separat în care specifici tipurile scripturilor tale Ruby.

Pentru a instala RBS trebuie să instalezi gem-ul rbs:

gem install rbs

Exemplul MinStack

Să adăugăm tipuri la următorul cod Ruby care se află în fișierul ./lib/minstack.rb:

# frozen_string_literal: trueclass MinStack  def initialize    @stack = []    @min_stack = []  end  def push(value)    minimum_value = @min_stack.empty? ? value : [value, @min_stack.last].min    @stack << value    @min_stack << minimum_value  end  def top    @stack.last  end  def pop    @stack.pop    @min_stack.pop  end  def get_minimum    @min_stack.last  endenddef main  puts 'Hello world'  stack = MinStack.new  stack.push(5)  stack.push(-10)  stack.push(-1)  stack.push(20)  stack.pop  puts "Top is #{stack.top&.to_s}"  puts "Min is #{stack.get_minimum&.to_s}"endmain if __FILE__ == $PROGRAM_NAME

Ar trebui să creezi un director nou în rădăcina proiectului numit sig și apoi să creezi fișierul minstack.rbs cu următorul conținut:

class MinStack  @stack: Array[Integer]  @min_stack: Array[Integer]  def initialize: () -> void  def push: (Integer value) -> void  def top: () -> Integer?  def pop: () -> Integer?  def get_minimum: () -> Integer?endclass Object  private def main: () -> voidend

Și acest lucru este destul de asemănător cu Python: ai function_name(): type -> return_value, iar ? înseamnă că tipul este opțional.

Dacă folosești RubyMine pentru dezvoltare, există integrare RBS, care îți arată ce variabile și funcții nu au tipuri.

Un alt lucru interesant aici este următorul cod:

class Object  private def main: () -> voidend

Fiecare metodă globală din Ruby devine o instanță privată a clasei Object. Dacă nu adăugăm aceste linii în fișierul .rbs, nu vom avea o metodă main cu tipuri.

Verificarea statică a tipurilor

Pentru verificarea statică a tipurilor cu RBS vei avea nevoie de un gem suplimentar numit steep. După instalare, trebuie să inițializezi steep.

gem install steepsteep init

Comanda init va genera un fișier Stepfile pe care îl poți edita pentru a configura verificarea statică a tipurilor:

target :app do  # 1. The code you want to type check  check "lib"      # Verify files in the lib directory  # 2. Where your RBS signatures are located  signature "sig"end

Acum poți executa step check și vei avea verificarea tipurilor:

➜  steep check # Type checking files:..No type error detected. 🫖

Poți, de asemenea, să schimbi parametrul din apelul push din int în string și să execuți din nou step check, iar vei primi o eroare.

lib/minstack.rb:35:13: [error] Cannot pass a value of type `::String` as an argument of type `::Integer`│   ::String <: ::Integer│     ::Object <: ::Integer│       ::BasicObject <: ::Integer││ Diagnostic ID: Ruby::ArgumentTypeMismatch│└   stack.push('5')               ~~~Detected 1 problem from 1 file

Tipuri cu Sorbet

Sorbet a fost creat acum 9 ani și este dezvoltat de Stripe. Spre deosebire de RBS, cu Sorbet vei adăuga de obicei tipurile direct în codul Ruby. Poți, de asemenea, să începi să adaugi tipuri treptat, punând #typed: false/true la începutul unui fișier Ruby.

Pentru a instala Sorbet, adaugă aceasta în Gemfile:

gem 'sorbet', :group => :developmentgem 'sorbet-runtime'gem 'sorbet-static-and-runtime'gem 'tapioca', require: false, :group => [:development, :test]

Apoi execută bundle install și srb init.

Exemplul MinStack

Putem refolosi codul MinStack din exemplul anterior și, după cum vezi, are deja tipuri adăugate:

# typed: true# frozen_string_literal: truerequire 'sorbet-runtime' # You must require this gemclass MinStack  extend T::Sig # Enables the 'sig' syntax  def initialize    # We explicitly tell Sorbet these are arrays of Integers    @stack = T.let([], T::Array[Integer])    @min_stack = T.let([], T::Array[Integer])  end  sig { params(value: Integer).void }  def push(value)    # If min_stack is empty, the new value is the min.    # Otherwise, compare the new value with the current min.    # We use T.must because we checked .empty?, so .last is technically safe,    # but Sorbet needs reassurance for the array logic.    current_min = @min_stack.last    minimum_value = current_min.nil? ? value : [value, current_min].min    @stack << value    @min_stack << minimum_value  end  # Returns Integer or nil (if stack is empty)  sig { returns(T.nilable(Integer)) }  def top    @stack.last  end  # Removes the top item and returns it (or nil)  sig { returns(T.nilable(Integer)) }  def pop    @min_stack.pop    @stack.pop  end  sig { returns(T.nilable(Integer)) }  def get_minimum    @min_stack.last  endenddef main  puts 'Hello world'  stack = MinStack.new  stack.push(5)  stack.push(-10)  stack.push(-1)  stack.push(20)  stack.pop  # We use safe navigation (&.) because top/get_minimum can return nil  puts "Top is #{stack.top&.to_s}"  puts "Min is #{stack.get_minimum&.to_s}"endmain if __FILE__ == $PROGRAM_NAME

Lucrul frumos la Sorbet este că toate tipurile se află în același fișier.

Verificarea statică a tipurilor

Poți verifica static tipurile codului Ruby executând srb check. Acest lucru ar fi util să fie adăugat într-un pipeline CI sau ca hook de pre-commit în git.

Vei vedea că nu există erori:

srb tc  No errors! Great job.

Și dacă modifici din nou push-ul: stack.push('5'), vei primi o eroare când rulezi srb tc:

srb tclib/minstack.rb:52: Expected Integer but found String("5") for argument value https://srb.help/7002    52 |  stack.push('5')                     ^^^  Expected Integer for argument value of method MinStack#push:    lib/minstack.rb:15:    15 |  sig { params(value: Integer).void }                       ^^^^^  Got String("5") originating from:    lib/minstack.rb:52:    52 |  stack.push('5')                     ^^^Errors: 1

Concluzie

Am aflat despre RBS și Sorbet și cum să le folosim explorând un exemplu simplu de cod.

RBS vine împreună cu Ruby 3.0 și nu oferă validare la rulare în mod implicit. Trebuie să scrii tipurile într-un fișier .rbs separat, iar acest lucru este excelent dacă scrii un gem personalizat, pentru că nu vrei să impui dependințe utilizatorilor tăi.

Sorbet este creat de Stripe și este folosit și în producție de ei și de alte companii. Scrii tipurile direct în fișierele Ruby și îl poți dezactiva pentru anumite fișiere. Aș folosi Sorbet dacă aș dezvolta o aplicație, fie un API, un CLI sau altă aplicație.

Mulțumesc că ai citit!

Referințe