Ruby tem function composition?
28-03-2020
Introdução
O que é function composition? Ou em bom português, composição de função. Mas vou me ater aos termos técnicos aqui em inglês, pois prefiro evitar traduções técnicas.
Em linguagens de programação funcionais, como Haskell, nós podemos definir funções e compor elas para criar novas funções, combinando seus comportamentos. Por exemplo:
fizzBuzz n = mod n 3 == 0 || mod n 5 == 0
fizzBuzzSum = sum . filter fizzBuzz
main = putStrLn $ show (fizzBuzzSum [1..999])
-- 233168
Quando queremos combinar 2 ou mais funções, nós podemos usar o operador .
em Haskell para combinar funções. O que acontece é que o valor vai ser encadeado e passado de uma função para a(s) outra(s), resultando em um valor depois de cada função ser aplicada.
Essa possibilidade nos dá um certo grau de flexibilidade, já que é possível definir novas funções re-utilizando funções já existentes para criar uma nova. Joinha para produtividade!
E o Ruby?
Ruby é uma linguagem multi-paradigma. É verdade que ela inclui algumas funcionalidades do paradigma funcional, mas não tem high order functions. Bem, pelo menos não como nós estamos acostumados a ver em Haskell e Javascript.
Ruby tem lambda
e Proc
objects. Ambos são intercambiáveis em muitos casos:
numbers = (1..999).to_a
fizz_buzz = -> (n) { n % 3 == 0 || n % 5 == 0 }
filter = -> (fn, array) { array.select(&fn) }
sum = proc { |array| array.reduce(:+) }
puts sum.call(filter.call(fizz_buzz, numbers))
# 233168
Se prestar atenção, fizz_buzz
é declarado como um lambda
, enquanto sum
é declarado como um Proc
object. Por baixo dos panos ambos são representados como um Proc
. Até aqui tudo bem.
E se quisermos compor isso? Se estiver utilizando Ruby 2.6+ é bem fácil:
fizz_buzz_sum = sum << filter.curry[fizz_buzz]
# composing the other way around
fizz_buzz_sum = filter.curry[fizz_buzz] >> sum
puts fizz_buzz_sum.call(numbers)
# 233168
Nada mal! Obrigado ao Ruby core team pelos operadores >>
e <<
para Proc
. :)
Mas Ruby é uma linguagem de programa orientada a objetos em sua essência. Devemos parar de usar objetos em favor de lambdas por todo lado?
Orientação a objetos… orientação a objetos em todo lugar!
Em linguagens orientadas a objetos, objetos e métodos são utilizados para representar comportamentos ao invés de funções. Esse é o jeito que nós utilizamos Ruby no dia a dia, porque em Ruby tudo é um objeto, lembra?
class FizzBuzz
def call(n)
n % 3 == 0 || n % 5 == 0
end
end
puts FizzBuzz.new.call(1)
# false
puts FizzBuzz.new.call(15)
# true
puts FizzBuzz.new.call(30)
# true
Dá para fazer o mesmo, mas como compor objetos com lambdas de uma maneira harmônica?
O to_proc
é um protocolo para converter um objeto para um objeto Proc
:
class FizzBuzz
def to_proc
-> (n) { n % 3 == 0 || n % 5 == 0 }
end
end
puts FizzBuzz.new.to_proc.call(3)
# true
puts FizzBuzz.new.call(3)
# true
Não é legal?!
puts (filter.curry[FizzBuzz.new] >> sum).call(numbers)
# 233168
Desse jeito nós podemos misturar objetos e lambdas.
Um jeito alternativo de composição
Bem, no Ruby 2.6+ nós temos os operadores >>
e <<
para compor lambdas
. Mas o que dizer das versões anteriores?
A beleza da programação funcional:
sum_fizz_buzz = -> (value) do
[filter.curry[FizzBuzz.new], sum].reduce(value) do |previous_result, object|
object.to_proc.call(previous_result)
end
end
puts sum_fizz_buzz.call(numbers)
# 233168
Quase tudo pode ser resolvido com apenas… funções. :)