Dá para fazer tipagem estática em Ruby?

Bom, resolvi começar uma série – coisas que você NUNCA quis fazer com Ruby, e tinha medo de perguntar. Basicamente, pensei em montar códigos absurdos de coisas que são completamente contra a filosofia da linguagem, ou que pelo menos são muito esquisitas, e publicar aqui os resultados. As regras são simples: os resultados devem ser testáveis (com RSpec, de preferência) e devem ser escritas puramente em Ruby, de preferência sem nenhuma biblioteca auxiliar (e, se for necessário usar, é obrigatório que a biblioteca tenha sido escrita puramente em Ruby também).

Como primeiro da série, vamos simular uma tipagem estática em Ruby. Como é impossível sobrescrever o operador “=” em Ruby, resolvi usar uma função – static – para simular o mesmo comportamento. Para simplificar, vamos meio que definir uma “variável global” com “static.”, e definir que esta terá tipagem estática. Começamos definindo uma função chamada “static”

def static
  Static.instance
end


Para facilitar mais ainda, esta Static será uma “Singleton Class“. A idéia é que a classe Static não possua métodos, e a partir do momento em que definimos uma nova variável (com static.variavel = 10, por exemplo), um novo método seja criado. Mas, se tentarmos chamar o método “static.variavel” antes dela ter sido declarada, deve dar um erro padrão. Portanto, uma possibilidade é declarar um “method_missing”, e identificar se o método que está sendo chamado termina com “=”, e possui exatamente um argumento:

require 'singleton'
class Static
  include Singleton
  def method_missing(method, *args, &b)
    method = method.to_s
    variable = method.chop
    is_setter = method.end_with?("=")
    super unless is_setter and args.size == 1
    super unless b.nil?
    self.class.send(:define_method, variable) { eval "@#{variable}" }
    self.class.send(:define_method, method) do |value|
      instance_variable_set(:"@#{variable}", value)
    end
    send method, args[0]
  end
end

Com isso, foi implementado um “getter” e “setter” virtual. Quando é atribuído um valor, estes métodos são criados, e a última linha chama o método setter recém-definido. Agora, basta apenas checar a tipagem:

require 'singleton'
class Static
  include Singleton
  def method_missing(method, *args, &b)
    method = method.to_s
    variable = method.chop
    is_setter = method.end_with?("=")
    super unless is_setter and args.size == 1
    super unless b.nil?

    original_class = args[0].class
    self.class.send(:define_method, variable) { eval "@#{variable}" }
    self.class.send(:define_method, method) do |value|
      unless value.class.ancestors.include?(original_class)
        raise ArgumentError, "invalid type - expecting #{original_class}"
      end

      instance_variable_set(:"@#{variable}", value)
    end
    send method, args[0]
  end
end

Este ficou sendo o código final – a variável original_class ganha a classe do primeiro valor do método, e é usada para a checagem de agora em diante. Caso a checagem de tipagem falhe, lança uma exceção. Simples, talvez até demais… então, que tal complicar um pouco? E se pudéssemos escrever uma classe assim:

class UmaClasse
  signature String, Fixnum
  def um_metodo(uma_string, um_numero)
    #Seu código aqui
  end
end

Agora, a coisa complicou consideravelmente… mas também é possível, se formos estudar certos “hook methods”: em Ruby, há certos métodos da classes (included, method_added, etc). A idéia é que, a partir do momento que alguém chamou o método da classe “signature”, o próximo método adicionado faça um comportamento especial. Basicamente, então:

class Class
  def signature(*classes)
    metaclass = class << self; self; end
    metaclass.send :define_method, :method_added do |method|
      #Comportamento Especial
    end
  end
end

Vale citar algumas coisas interessantes aqui: reabrimos a classe Class, para todas as classes terem este comportamento. Definimos um método (da instância – lembramos que todas as classes são instâncias da classe Class) chamado “signature”, que por padrão define um método chamado “method_added”, um hook que é executado sempre que um método for definido. Este método deveria ser definido como método da classe, não da instância, e é por isso que atribuímos o valor da variável “metaclass”. Mais informações sobre isso em outro post, talvez futuro (por hora, confie em mim que isso definirá um método da classe). Vale lembrar que, dentro do método “signature”, o valor de “self” é a classe que está sendo definida (no exemplo que citamos, o valor de “self” é “UmaClasse”), ou seja, no exemplo que tínhamos, o método seria UmaClasse.method_added.

A partir deste ponto, precisamos implementar toda a funcionalidade. Para tal, em primeiro lugar, precisamos remover a definição do “method_added”, senão todas as próximas funções terão a “assinatura” definida por “signature”, o que não é desejável (ao menos, não agora). Para efeito didático, encare que as próximas linhas irão para dentro do bloco definido por “metaclass.send :define_method…”, ou seja, aonde está o comentário.

      metaclass.send :remove_method, :method_added
      #Comportamento Especial

O próximo passo, é sobrescrever o método que foi definido para ele fazer a checagem da assinatura do método. Para tal, basta obter o método que definimos na instância (pois não vamos apenas sobrescrevê-lo – precisamos, de alguma forma, chamar o método original):

      metaclass.send :remove_method, :method_added
      unbound_method = self.instance_method(method)
      #Comportamento Especial

Depois redefinimos o método, fazendo a checagem de tipagem:

      unbound_method = self.instance_method(method)
      define_method(method) do |*method_args| #Redefinição do método
        classes.each_with_index do |klass, index| #Para cada classe definida no "signature"
          unless method_args[index].is_a?(klass) #Checagem de tipo
            raise ArgumentError, "expecting #{klass} for parameter #{index} of method #{method}"
          end
        end
        #Comportamento Especial
      end

E por fim, rodamos o comando original do método. Isto pode ser feito rodando o método “bind”, do unbound_method, no próprio objeto instanciado. No fim, o código de classe inteiro fica desta forma:

class Class
  def signature(*classes)
    metaclass = class << self; self; end
    metaclass.send :define_method, :method_added do |method|
      metaclass.send :remove_method, :method_added
      unbound_method = self.instance_method(method)
      define_method(method) do |*method_args|
        classes.each_with_index do |klass, index|
          unless method_args[index].is_a?(klass)
            raise ArgumentError, "expecting #{klass} for parameter #{index} of method #{method}"
          end
        end
        unbound_method.bind(self).call(*method_args)
      end
    end
  end
end

Chocante, não? O código-fonte desta loucura está no meu github: http://github.com/mauricioszabo/Things-you-never-wanted-to-know/tree/master/static_typing/

Lembrem-se, por favor: isto é APENAS uma experiência. Não é recomendado, nem saudável, usar isso em projetos de produção! Isto é completamente contra o “ruby way”, assim como provavelmente os próximos posts desta categoria, portanto vale apenas como objeto de estudo. Por fim, é interessante o poder da linguagem: ela, sozinha, tem mecanismos para até mesmo quebrar seus próprios paradigmas. Vale a pena estudar mais, para saber os limites.

Advertisements
This entry was posted in Coisas que você nunca quis fazer com Ruby. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s