Blog

LISTAR TODOS OS POSTS - Assine os feeds dos posts e comentários

Compondo funções em Ruby

Publicado/atualizado em 12/09/2007 04:52

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.

Permalink: http://eustaquiorangel.com/posts/460

salvar no del.icio.ussalvar no diggsalvar no rec6 Veja o que estão dizendo sobre isso.

Comente

Linhas em branco viram saltos de linha. Se você quiser mostrar algum código, por favor use o pastebin e informe a URL.

*

*

Responda: Qual é a palavra que significa 'livro', em Inglês?  
Clique aqui se não souber essa resposta!

* campos obrigatórios

Comentários

1 - Shairon disse em 10/09/2007 14:04

Uai TaQ esse (class << self;end;).class_eval é a mesma coisa de Kernel.class_eval neste contexto. Fica mais "bonitinho" :)


2 - TaQ disse em 10/09/2007 14:31

Shairon, discordo do bonitinho. :-) Usando Kernel.class_eval funciona, mas me aparenta ficar mais engessado, rígido e redundante escrevendo o nome da classe dentro dela mesma, apesar de funcionalmente *nesse caso* ser a mesma coisa.

O "class << self; self; end;" dá uma característica mais dinâmica, afinal, ele se auto-avalia para descobrir onde está, o que me dá um pouco mais de conforto do que deixar fixo. Imagine se um belo e louco dia resolvem mudar o nome do Kernel para, sei lá, Nucleus, o seu código ficaria quebrado se você não saísse trocando tudo de nome. Apesar da operação de substituição de strings ser relativamente barata, eu prefiro trocar poucas do que muitas para evitar dores-de-cabeça. Lógico que chutei o balde na possibilidade do exemplo, mas acho que deu para entender a lógica da coisa. ;-)

Particularmente eu prefiro dessa maneira que escrevi aqui, que chega até a ser um "reflexo" de quem já acostumou com esse tipo de coisa, e ainda dá um gancho para deixar no ar o conceito de metaclasses. ;-)


3 - TaQ disse em 10/09/2007 15:42

Ah, peraí, detalhe importante não mencionado no meu comentário anterior: o Kernel.class_eval insere um método na classe, e o "class << self; end; end;" está inserindo um método na *instância*. Se quisermos usar na classe sem "engessar", podemos usar o self.class.class_eval. Vai de acordo com o gosto, mas vou alterar ali em cima para deixar mais legível e com poucas dúvidas. :-)


4 - Shairon disse em 11/09/2007 04:35

Agora ficou doido


5 - João disse em 11/09/2007 15:49

Parece que já vi isso em algum lugar... Post no blog do Akita On Rails no dia 7/09.

Acho q os posts deste blog deveriam ser originais, não baseado em idéias de outros blogs. ;-)


6 - TaQ disse em 11/09/2007 17:50

João, não sei se você prestou atenção (deu rima isso hein!), mas o artigo *original* é esse do Tom Moertel que eu mencionei logo ali na primeira linha do post como um hyperlink, e realmente foi de onde eu tirei a idéia de alterar o código para deixar de um jeito que achei melhor, o que se não deixa o código totalmente original também não comete nenhum pecado pela tentativa de melhoria.

Se você já havia reparado nisso, deixa eu dizer que não vejo problemas em exercitar um pouco a cachola em buscar soluções que achamos mais adequadas baseando-se em alguma solução original e publicá-las, desde que *dando o crédito para o artigo original*.

Se você vê problemas nisso, vai ter que ter um pouco de paciência aqui, pois eu gosto de fazer alguns refactorings/alterações de código em alguns posts, o que se não deixa o post totalmente original pelo fato de ter se baseado em outro pelo menos (acredito eu) cria mais valor agregando mais algumas coisas em relação à código e idéias. Inclusive eu inseri a referência desse post no original para o caso do autor lá quiser começar uma "escovação de bits saudável" sobre o assunto. :-)

Sobre a sua referência para o outro blog, você pode ver que nesse caso se trata de uma *tradução* do artigo original, como descrito no título do post e no crédito do autor, logo abaixo do título, onde consta a mesma URL do começo desse meu post. Pode-se até confundir essa tradução com um artigo original do autor desse blog, mas de original ali há somente o Português e os comentários.

E mesmo se essa tradução fosse o post original, não veria problemas em publicar essas alterações que mostrei aqui. Ou você não notou que o código aqui está diferente? ;-)


Anterior Próximo Últimos Índice