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.

Mensagem de erro de tipagem pelo VS Code em tempo real.

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

0 Comentário