segunda-feira, 25 de julho de 2011

Pequenas coisas que incomodam em Ruby

Ruby é uma linguagem maravilhosa, mas tem alguns poréns. O que eu vou citar é algo bem pequeno mas a linguagem está repleta de coisas do tipo.

> class Foo
>   def bar; @bar; end
>   def bar=(other)
>     puts "bar= has been called"
>     @bar = other
>   end
> end

Basicamente escrevemos um setter para um variável.

> foo = Foo.new
> foo.bar = 1
bar= has been called
=> 1 

Ok, exatamente como esperávamos.

> foo.bar = foo.bar || 2
bar= has been called
=> 1 

Ok, sem surpresas ainda.

> foo.bar ||= 2
=> 1 

Ops, notem que bar= não foi chamado.

Portanto, ao contrário do que muitos pensam foo.bar ||= 2 é diferente de foo.bar = foo.bar || 2.

foo.bar ||= 2 só realiza a atribuição se bar não avaliar para uma expressão verdadeira.

É algo muito bobo e muito sútil, mas se o programador realizar alguma computação na atribuição, essa computação pode não ser executada em alguns casos.

Isso não é um grande problema, porém existem diversas coisas do tipo na linguagem. O Matz diz que a linguagem deve ter o comportamento que o programador espera, isso é bom porque a linguagem torna-se natural, mas e casos como esse, o que o programador espera?

Outra coisa decorrente é que isto dificulta outras implementações Ruby 100% compatíveis.

quinta-feira, 27 de janeiro de 2011

Um novo Object#tap

Implementação atual de tap:
class Object
  def tap
    yield(self)
    self
  end
end
Mas o Daniel aqui discorda. Minha proposta é:
class Object
  def tap
    yield(self)
  end

  def tee
    yield(self)
    self
  end
end
Object#tap
altera a method chain
Object#tee
não altera a method chain (atual Object#tap)
Para Enumerable, nós temos Enumerable#collect, que funciona exatamente igual ao meu tap.
[1,2,3].collect {|i| i + i} => [2,4,6]
Porém não temos isso para objetos:
1.tap {|i| i + i } => 2
Object#tap executa um bloco na method chain sem altera-la.
[1,2,3].collect {|i| i * i }.tap {|a| puts a.size }.inject {|sum, i| sum + i } => 14 (escreve 3 na STDOUT antes do retorno)
Porém se você quer justamente passar um bloco apenas para alterar a method chain? Não deveria ser justamente essa a função de Object#tap?
[1,2,3].collect {|i| i * i }.tap {|a| a + a }.inject {|sum, i| sum + i } => 28
A origem do atual Object#tap vem do comando tee. Portanto, nada mais justo que:
class Object
  alias tee tap
  def tap
    yield(self)
  end
end
É preciosismo e a discussão é meramente teórica, já que podemos implementar este novo Object#tap com outro nome e evitar o alias, o que exigiria um refactoring de todo código que utiliza Object#tap. Possíveis nomes para meu Object#tap: get, come, become, go e grow. Viajei muito?