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, &amp;b) method = method.to_s variable = method.chop is_setter = method.end_with?(&quot;=&quot;) super unless is_setter and args.size == 1 super unless b.nil? <pre><code>original_class = args[0].class self.class.send(:define_method, variable) { eval &amp;quot;@#{variable}&amp;quot; } self.class.send(:define_method, method) do |value| unless value.class.ancestors.include?(original_class) raise ArgumentError, &amp;quot;invalid type - expecting #{original_class}&amp;quot; end instance_variable_set(:&amp;quot;@#{variable}&amp;quot;, value) end send method, args[0] </code></pre> 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(<em>classes) metaclass = class &lt;&lt; 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 |</em>method_args| classes.each_with_index do |klass, index| unless method_args[index].is_a?(klass) raise ArgumentError, &quot;expecting #{klass} for parameter #{index} of method #{method}&quot; 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.