Blog do TaQ

Compondo funções em Ruby

Publicado em Developer

Estava vendo aqui um link sobre composição de funções em Ruby e pensando em uma das poucas coisas que eu acho "feinhas" na linguagem, o jeito que chamamos uma Proc, com call(argumento) ou [argumento]. No artigo o autor mostra como fazer isso em Haskell usando o operador . (ponto) e mostra como sobrecarregar um operador em Ruby (no caso, o *, asterisco) para fazer algo similar.

Eu acho que sobrecarregar o * fica meio estranho pois volta e meia podemos ver ele como uma multiplicação (eu fiquei dando "curto-circuito" na interpretação por uns bons minutos), mas tudo bem, vamos continuar.

Logo após ele monta uma Proc nova com o resultado das outras duas Proc's. Funciona legal, mas eu ainda fiquei incomodado com chamar as Proc's daquela maneira e não de uma maneira mais transparente como em Haskell, aí fiz o seguinte código:

 1 def inc(p)
 2    p+1
 3 end
 4 
 5 def twice(p)
 6    p*2
 7 end
 8 
 9 module Kernel
10    def compose(*values)
11       name = values.shift
12       self.class.class_eval do
13          define_method(name) do |param|
14             result ||= param
15             values.reverse.inject(result) {|memo,met| memo = send(met,*memo)}
16          end
17       end
18 end
19 end
20 
21 compose(:twiceOfInc,:twice,:inc)
22 puts twiceOfInc(2)
   => 6

Rodando isso vamos ter o mesmo resultado que o código original, com (em minha opinião) um pouco mais de clareza na definição do método novo, usando compose, e na chamada desse método, sem a necessidade de call ou []. Apesar de usar um eval ali no meio para adicionar o método na classe, a complexidade fica dentro do método compose e não no uso criando e chamando as Proc's da maneira como foi apresentado no artigo original, a causa do que eu acho "feinho". Mas essa é a minha opinião hein! :-)

Com o uso de alguns splats (o famigerado * nesse caso), podemos ter um comportamento que nos permite fazer umas coisas malucas como:

 1 def inc(p)
 2    p+1
 3 end
 4 
 5 def twice(p)
 6    p*2
 7 end
 8 
 9 def half(p)
10    p/2.0
11 end
12 
13 def odd_and_even(p)
14    (0..p).to_a.partition {|item| item%2==1}
15 end
16 
17 def print_odds_and_evens(odds,evens)
18    puts "odds : #{odds.join(',')}"
19    puts "evens: #{evens.join(',')}"
20 end
21 
22 module Kernel
23    def compose(*values)
24       name = values.shift
25       self.class.class_eval do
26          define_method(name) do |param|
27             result ||= param
28             values.reverse.inject(result) {|memo,met| memo = send(met,*memo)}
29          end
30       end
31    end
32 end
33 
34 compose(:crazy_thing,:print_odds_and_evens,:odd_and_even,:half,:twice,:inc)
35 crazy_thing(10)
=> odds : 1,3,5,7,9,11
   evens: 0,2,4,6,8,10

Concatenei mais alguns métodos (print_odds_and_evens,:odd_and_even,:half) sendo que o penúltimo retorna mais de um valor e o último recebe esses valores de boa.

Atualizado: Se é para o bem do povo e felicidade geral das coisas legíveis, eu retirei o class << self; self; end; (vide comentários abaixo) e troquei para o self.class.class_eval, que junta as duas coisas em uma só: tira o método adicionado na instância corrente e propicia a inserção do compose na classe corrente sem usar o nome hardcoded da dita cuja (e consequentemente nas suas classes filhas).

Atualizado: O maluco do Shairon fez um post aqui sobre isso usando Postscript e ficou muito legal! Vale a pena conferir.


Tags:


Comentários

comments powered by Disqus

Twitter