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 ' + nameRuby 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 rbsExemplul 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_NAMEAr 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: () -> voidendFiecare 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 initComanda 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"endAcum 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 fileTipuri 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_NAMELucrul 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: 1Concluzie
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!