Tipagem estática em Ruby on Rails com RBS
O RBS é um recurso, incluído por padrão no Ruby a partir da versão 3, que permite definir os tipos de um arquivo Ruby de forma estática em um novo arquivo de extensão .rbs. Neste artigo irei mostrar como usar o RBS em uma aplicação Ruby on Rails e configurar…
O RBS é um recurso, incluído por padrão no Ruby a partir da versão 3, que permite definir os tipos de um arquivo Ruby de forma estática em um novo arquivo de extensão .rbs
. Neste artigo irei mostrar como usar o RBS em uma aplicação Ruby on Rails e configurar um editor de texto como o VS Code para exibir as informações de tipagem.
Como o RBS funciona
Vamos começar configurando o Steep, ele é como um validador que checa a estrutura da tipagem de um arquivo Ruby e retorna possíveis erros e avisos. Em uma aplicação Rails iremos instalar a gem steep
pelo Gemfile:
gem "steep", require: false
Na linha do comando utilizar o seguinte comando para criar um arquivo de configuração da análise do Steep sobre a aplicação:
steep init
O comando irá criar o arquivo Steepfile, nele iremos configurar inicialmente para o Steep só analisar os arquivos ruby na pasta service:
target :app do
check "app/services"
signature "sig"
library "pathname"
end
Vamos criar uma classe simples em Ruby dentro da pasta service para entendermos primeiro como funciona o RBS num PORO
class Calculadora
def somar(valor_um:, valor_dois:)
valor_um + valor_dois
end
end
Em seguida podemos rodar o Steep para analisar nossos arquivos com:
steep check
Como resultado devemos receber um warning informando que a classe Calculadora
não tem seus tipos assinalados:
# Type checking files:
...........................................................F........................
app/services/calculadora.rb:1:6: [warning] Cannot find the declaration of class: `Calculadora`
│ Diagnostic ID: Ruby::UnknownConstant
│
└ class Calculadora
~~~~~~~~~~~
Para resolver isso podemos gerar os tipos iniciais da Calculadora
com o Prototype:
rbs prototype rb app/services/calculadora.rb
Cole o conteúdo para dentro da pasta sig/
rbs prototype rb app/services/calculadora.rb -o sig/app/
Ao rodar o steep check
novamente devemos receber nenhum erro detectado
Para quem utiliza o VS Code recomendo a extensão RBS Syntax para colorização e syntaxe nos arquivos .rb: https://marketplace.visualstudio.com/items?itemName=soutaro.rbs-syntax
O prototype gerou alguns dos tipos da calculadora como untyped
, mas para melhorar podemos defini-los como Float
da seguinte forma:
class Calculadora
def somar: (valor_um: Float, valor_dois: Float) -> Float
end
Ao rodar o steep check devemos continuar sem nenhum erro de tipagem. Porém se irmos em calculadora e fazermos um to_s
no retorno de somar. Deveremos receber um erro informanto que o tipo retornado será um diferente do esperado:
def somar(valor_um:, valor_dois:)
(valor_um + valor_dois).to_s
end
Com isso, outros erros como nomes de variáveis errados, atribuição de valores nil
para variáveis obrigatórios já serão reportados, deixando a aplicação mais segura, porém não substitui os testes automatizados que não devam ser deixados de lado de forma alguma.
Vale ressaltar que o RBS não altera o comportamento do Ruby, e embora sejam relatados alguns erros de tipagem, em tempo de execução podem não ocorrer.
Além de receber informações sobre a tipagem pela linha de comando, o Steep também possui formas de exibir em tempo real em alguns editores de texto. No VS Code só é preciso instalar a extenção Steep https://marketplace.visualstudio.com/items?itemName=soutaro.steep-vscode. para funcionar.
Em alguns momentos a extensão pode não exibir as mensagens, para resolver isso podemos reiniciar seu carregamento com: Ctrl + Shift + P
e Steep: Restart all
RBS Rails
Porém como definiremos todos os métodos que são criados magicamente pelo Active Record do Rails em tempo de execução? Para resolvermos isso existe a gem RBS Rails.
Vamos supor que em nosso projeto temos models criados com os seguintes comandos:
rails g model cliente nome email senha
rails g model telefone ddd numero cliente:references
class Cliente < ApplicationRecord
has_many :telefones, dependent: :destroy
end
class Telefone < ApplicationRecord
belongs_to :cliente
end
No Steepfile
iremos alterar para que ele analise todos os arquivos dentro da pasta app deixando o check com apenas “app”
:
Com a gem instalada, precisamos rodar o seguinte comando para criar um rake de execução para os geradores da gem:
bin/rails g rbs_rails:install
Para gerar os tipos dos arquivos Rails, por exemplo, os models do seu projeto, podemos usar o seguinte comando:
rake rbs_rails:all
Então podemos usar esses comandos para gerar os tipos dos arquivos da gem Rails:
bundle exec rbs collection init
bundle exec rbs collection install
Eles serão criados em .gem_rbs_collection/
.
Os arquivos RBS dos models serão criados em sig/rbs_rails/app/models/
. Todos os métodos que o Rails criar para colunas, associações, por exemplo, serão mapeados:
...
def nome: () -> String?
def nome=: (String?) -> String?
def nome?: () -> bool
def nome_changed?: () -> bool
def nome_change: () -> [ String?, String? ]
def nome_will_change!: () -> void
def nome_was: () -> String?
...
Exemplos de como a tipagem pode ser útil
Vamos supor que queremos pegar o DDD mais recente de um cliente com o seguinte método:
def ddd_principal
telefones.order(:created).first.ddd
end
Provavelmente funcionará, mas numa situação que o cliente não tenha telefones cadastrados irá ocorrer erro undefined method `ddd` for nil:NilClass
ao acessar o DDD pelo primeiro telefone que é nil
. Isso é um exemplo simples, mas coisas como essa podem passar despercebidas muitas vezes. Entretanto, se usarmos a checagem da tipagem iremos receber um aviso de erro:
app/models/cliente.rb:5:36: [error] Type `(::Telefone | nil)` does not have method `ddd`
│ Diagnostic ID: Ruby::NoMethod
│
└ telefones.order(:created).first.ddd
Então para resolver, podemos usar safe navigation
ou utilizar um bang como first!
, assim ficará claro qual comportamento o código terá.
Pontos negativos da tipagem estática
Metaprogramação
Métodos criados em tempo de execução são difíceis de tipar, mas não são impossíveis como vimos no caso de métodos de models que implementam o ActiveRecord do Rails.
Ducktype
Com a tipagem dinâmica, o Ruby não se importa com o tipo de um método se ele se comportar como o desejado. Porém, com a tipagem estática isso se perde, mas, por outro lado, ganhamos outras opções como interfaces que definem os métodos que as classes devem implementar.
Maior complexidade
Será gasto mais esforços com a criação e manutenção dos arquivos .rbs
, principalmente em projetos pequenos pode não haver necessidade.
Conclusão
Vimos que é possível utilizar o Ruby com tipagem estática e configurá-lo num editor de texto como o VS Code, embora seja algo relativamente novo na linguagem, já temos recursos interessantes, embora possam ainda não estar com maturidade. Deixe sua opinião a respeito do assunto nos comentários. Até mais,
Referências
RBS: A New Ruby 3 Typing Language in Action | AppSignal Blog
vscode と steep を連携させて ruby の型検査をする方法を紹介します
https://github.com/soutaro/steep
https://github.com/ruby/typeprof
Understanding RBS, Ruby’s new Type Annotation System
An Exploration of RBS by Ruby: Is it Production-Ready?
Ease Into Ruby 3’s Static Typing Powers With RBS, TypeProf and Steep – Semaphore