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.
Comentários
Comentários fechados.
Artigos anteriores
- Pull requests em modo raiz - sex, 22 de dezembro de 2023, 09:57:09 -0300
- Qual a idade do seu repositório? - ter, 27 de dezembro de 2022, 12:50:35 -0300
- Utilizando ctags em projetos Rails mais recentes - qui, 24 de junho de 2021, 08:23:43 -0300
- Fazendo o seu projeto brotar - seg, 15 de julho de 2019, 08:57:05 -0300
- Learn Functional Programming with Elixir - sex, 02 de março de 2018, 18:47:13 -0300
- Ambiente mínimo - Driver Driven Development - qua, 23 de agosto de 2017, 15:15:03 -0300
- Ambiente mínimo - repositórios de código - dom, 16 de abril de 2017, 13:02:14 -0300
- Ambiente mínimo - terminal e navegador - dom, 02 de abril de 2017, 21:43:29 -0300
- Utilizando muitas gems no seu projeto? - sáb, 29 de outubro de 2016, 11:57:55 -0200
- Desenvolvedores e inteligência artificial - seg, 11 de julho de 2016, 09:09:38 -0300
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? ;-)
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. ;-)
Agora ficou doido
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. :-)
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. ;-)
Uai TaQ esse (class << self;end;).class_eval é a mesma coisa de Kernel.class_eval neste contexto. Fica mais "bonitinho" :)