do..end vs llaves para bloques en Ruby

155

Tengo un compañero de trabajo que intenta convencerme activamente de que no debería usar do..end y, en cambio, usar llaves para definir bloques multilínea en Ruby.

Estoy firmemente en el campo de usar solo llaves para las frases cortas y terminar ... para todo lo demás. Pero pensé que llegaría a la comunidad en general para obtener alguna resolución.

Entonces, ¿cuál es y por qué? (Ejemplo de un código de hombro)

context do
  setup { do_some_setup() }
  should "do somthing" do
    # some more code...
  end
end

o

context {
  setup { do_some_setup() }
  should("do somthing") {
    # some more code...
  }
}

Personalmente, solo mirar lo anterior responde la pregunta para mí, pero quería abrir esto a la comunidad en general.

Blake Taylor
fuente
3
Me pregunto, pero ¿cuáles son los argumentos de sus compañeros de trabajo para usar frenillos? Parece más una preferencia personal que una cosa lógica.
Daniel Lee
66
Si quieres una discusión, conviértela en un wiki comunitario. A su pregunta: es solo un estilo personal. Prefiero las llaves ya que se ven más nerd.
halfdan
77
No es una cosa de preferencia, hay razones sintácticas para usar una sobre la otra además de razones estilísticas. Varias de las respuestas explican por qué. No usar el correcto puede causar errores muy sutiles que son difíciles de encontrar si se toma otra opción "estilística" para nunca usar paréntesis de ajuste para los métodos.
The Tin Man
1
Las "condiciones de borde" tienen la mala costumbre de acercarse sigilosamente a las personas que no las conocen. Codificar a la defensiva significa muchas cosas, incluida la decisión de usar estilos de codificación que minimicen la posibilidad de encontrarse con los casos. TÚ puedes estar al tanto, pero el tipo que dos personas después de ti podrían no ser después de que el conocimiento tribal haya sido olvidado. Desafortunadamente, suele suceder en entornos corporativos.
The Tin Man
1
@tinman Estos tipos de condiciones de borde son manejados por TDD. Es por eso que Rubist puede escribir código como este y descansar por la noche sabiendo que tales errores no existen en su código. Cuando se desarrolla con TDD o BDD y se comete tal error de precedencia, el rojo que se muestra en la pantalla es lo que nos recuerda el "conocimiento tribal". Por lo general, la solución es agregar un par de padres en algún lugar, lo cual es totalmente aceptable dentro de las convenciones estándar. :)
Blake Taylor

Respuestas:

247

La convención general es usar do..end para bloques de varias líneas y llaves para bloques de una sola línea, pero también hay una diferencia entre los dos que se puede ilustrar con este ejemplo:

puts [1,2,3].map{ |k| k+1 }
2
3
4
=> nil
puts [1,2,3].map do |k| k+1; end
#<Enumerator:0x0000010a06d140>
=> nil

Esto significa que {} tiene una precedencia más alta que do..end, así que tenlo en cuenta al decidir qué quieres usar.

PD: Un ejemplo más a tener en cuenta mientras desarrollas tus preferencias.

El siguiente código:

task :rake => pre_rake_task do
  something
end

realmente significa:

task(:rake => pre_rake_task){ something }

Y este código:

task :rake => pre_rake_task {
  something
}

realmente significa:

task :rake => (pre_rake_task { something })

Entonces, para obtener la definición real que desea, con llaves, debe hacer:

task(:rake => pre_rake_task) {
  something
}

Tal vez el uso de llaves para los parámetros es algo que desea hacer de todos modos, pero si no lo hace, probablemente sea mejor usar do..end en estos casos para evitar esta confusión.

Pan Thomakos
fuente
44
Voto por usar siempre paréntesis alrededor de los parámetros del método para eludir todo el asunto.
The Tin Man
@ The Tin Man: ¿Incluso usas paréntesis en Rake?
Andrew Grimm
9
Es un hábito de programación de la vieja escuela para mí. Si un idioma admite paréntesis, los uso.
The Tin Man
8
+1 para señalar el problema de precedencia. A veces recibo excepciones inesperadas cuando intento convertir do; fin a {}
idrinkpabst
1
¿Por qué puts [1,2,3].map do |k| k+1; endsolo imprime el enumerador, es decir, una cosa? ¿No se pasan dos argumentos aquí, a saber, [1,2,3].mapy do |k| k+1; end?
ben
55

De la programación de Ruby :

Los frenos tienen una alta precedencia; hacer tiene una baja precedencia. Si la invocación del método tiene parámetros que no están encerrados entre paréntesis, la forma de llave de un bloque se unirá al último parámetro, no a la invocación general. El formulario do se unirá a la invocación.

Entonces el código

f param {do_something()}

Vincula el bloque a la paramvariable mientras el código

f param do do_something() end

Vincula el bloque a la función f.

Sin embargo, esto no es un problema si incluye argumentos de función entre paréntesis.

David Brown
fuente
77
+1 "Sin embargo, esto no es un problema si incluye argumentos de función entre paréntesis". Lo hago por costumbre, en lugar de confiar en el azar y el capricho del intérprete. :-)
the Tin Man
44
@ theTinMan, si su intérprete usa el azar en su analizador, necesita un nuevo intérprete.
Paul Draper
@PaulDraper: de hecho, pero no todos los idiomas están de acuerdo con este tema. Pero (creo) es bastante raro que los padres no "hagan su trabajo", por lo que el uso de los padres universalmente le evita tener que recordar las decisiones "aleatorias" tomadas por los diseñadores de idiomas, incluso si los implementadores de compiladores e intérpretes los ejecutan fielmente .
dlu
15

Hay algunos puntos de vista sobre esto, es realmente una cuestión de preferencia personal. Muchos rubíes toman el enfoque que tú haces. Sin embargo, otros dos estilos que son comunes es usar siempre uno u otro, o usar {}para bloques que devuelven valores, y do ... endpara bloques que se ejecutan para efectos secundarios.

GSto
fuente
2
No es solo una cuestión de preferencia personal. La vinculación de parámetros puede causar errores sutiles si los parámetros del método no se incluyen entre paréntesis.
The Tin Man
1
Actualmente no estoy haciendo la última opción, pero estoy haciendo +1 por mencionar que algunas personas toman ese enfoque.
Andrew Grimm
Avdi Grimm aboga por esto en una publicación de blog: devblog.avdi.org/2011/07/26/…
alxndr
8

Hay un beneficio importante para las llaves: muchos editores tienen MUCHO más fácil hacer coincidirlas, lo que hace que ciertos tipos de depuración sean mucho más fáciles. Mientras tanto, la palabra clave "do ... end" es un poco más difícil de igualar, especialmente porque "end" también coincide con "if" s.

GlifoGrifo
fuente
RubyMine, por ejemplo, resalta las do ... endpalabras clave de forma predeterminada
ck3g del
1
Si bien prefiero las llaves, no veo ninguna razón para que un editor tenga problemas para encontrar una cadena de caracteres de 2/3 frente a un solo carácter. Aunque existe el argumento de que la mayoría de los editores ya coinciden con llaves, mientras que do ... endes un caso especial que requiere una lógica específica del idioma.
Chinoto Vokro
7

La regla más común que he visto (más recientemente en Eloquent Ruby ) es:

  • Si es un bloque de varias líneas, use do / end
  • Si es un bloque de una sola línea, use {}
Peter Brown
fuente
7

Estoy votando por hacer / terminar


La convención es do .. endpara { ... }líneas múltiples y para líneas simples.

Pero me gusta do .. endmás, así que cuando tengo un forro único, lo uso de do .. endtodos modos, pero lo formateo como de costumbre para hacer / terminar en tres líneas. Esto hace a todos felices.

  10.times do 
    puts ...
  end

Uno de los problemas { }es que es poesía en modo hostil (porque se unen estrechamente al último parámetro y no a toda la llamada al método, por lo que debe incluir métodos parensos) y, en mi opinión, simplemente no se ven tan bien. No son grupos de enunciados y chocan con constantes hash para facilitar la lectura.

Además, he visto suficientes { }programas en C. El camino de Ruby, como siempre, es mejor. Hay exactamente un tipo de ifbloque, y nunca tiene que volver atrás y convertir una declaración en una declaración compuesta.

DigitalRoss
fuente
2
"He visto suficientes {} en los programas en C" ... y Perl. Y, +1 por varias declaraciones allí y por defender los estilos de codificación zen de Ruby. :-)
the Tin Man
1
Estrictamente hablando, hay otra sintaxis "if": el postfix "if" ( do_stuff if x==1), que es un poco molesto convertir a la sintaxis regular. Pero esa es otra discusión.
Kelvin
Hay prefijo if, postfix if, postfix unless(también una especie de if, si no) y una línea ifcon then. La forma de Ruby es tener muchas maneras de expresar, de hecho, algo que siempre es lo mismo, mucho peor que lo que ofrece C que sigue reglas muy simples y estrictas.
Mecki
2
Entonces, si nos gusta el {} en C, ¿eso significa que también deberíamos usarlos en Ruby?
Warren Dew
6

Un par de rubíes influyentes sugieren usar llaves cuando usa el valor de retorno, y hacer / finalizar cuando no lo hace.

http://talklikeaduck.denhaven2.com/2007/10/02/ruby-blocks-do-or-brace (en archive.org)

http://onestepback.org/index.cgi/Tech/Ruby/BraceVsDoEnd.rdoc (en archive.org)

Esto parece una buena práctica en general.

Modificaría este principio un poco para decir que debe evitar usar do / end en una sola línea porque es más difícil de leer.

Debe tener más cuidado al usar llaves, ya que se unirá al parámetro final de un método en lugar de la llamada completa al método. Simplemente agregue paréntesis para evitar eso.

Kelvin
fuente
2

En realidad, es una preferencia personal, pero dicho esto, durante los últimos 3 años de mis experiencias con el rubí, lo que he aprendido es que el rubí tiene su estilo.

Un ejemplo sería, si viene de un fondo JAVA, para un método booleano que podría usar

def isExpired
  #some code
end 

observe el caso del camello y, con mayor frecuencia, el prefijo 'es' para identificarlo como un método booleano.

Pero en el mundo rubí, el mismo método sería

def expired?
  #code
end

así que personalmente creo que es mejor ir con 'ruby way' (pero sé que lleva un tiempo entenderlo (me llevó alrededor de 1 año: D)).

Finalmente iría con

do 
  #code
end

bloques

sameera207
fuente
2

Puse otra respuesta, aunque ya se señaló la gran diferencia (precedencia / encuadernación), y eso puede causar problemas difíciles de encontrar (el Hombre de hojalata y otros lo señalaron). Creo que mi ejemplo muestra el problema con un fragmento de código no tan habitual, incluso los programas experimentados no se leen como los domingos:

module I18n
    extend Module.new {
        old_translate=I18n.method(:translate)
        define_method(:translate) do |*args|
            InplaceTrans.translate(old_translate, *args)
        end
        alias :t :translate
    }
end

module InplaceTrans
    extend Module.new {
        def translate(old_translate, *args)
            Translator.new.translate(old_translate, *args)
        end
    }
end

Luego hice un código de embellecimiento ...

#this code is wrong!
#just made it 'better looking'
module I18n
    extend Module.new do
        old_translate=I18n.method(:translate)
        define_method(:translate) do |*args|
            InplaceTrans.translate(old_translate, *args)
        end
        alias :t :translate
    end
end

si cambia {}aquí do/endobtendrá el error, ese método translateno existe ...

Por qué sucede esto se señala aquí más de uno: precedencia. ¿Pero dónde poner llaves aquí? (@ The Tin Man: siempre uso llaves, como tú, pero aquí ... supervisado)

entonces cada respuesta como

If it's a multi-line block, use do/end
If it's a single line block, use {}

es incorrecto si se usa sin el "PERO ¡Vigile los frenos / precedencia!"

de nuevo:

extend Module.new {} evolves to extend(Module.new {})

y

extend Module.new do/end evolves to extend(Module.new) do/end

(cualquiera que sea el resultado de extender con el bloque ...)

Entonces, si desea usar do / end, use esto:

#this code is ok!
#just made it 'better looking'?
module I18n
    extend(Module.new do 
        old_translate=I18n.method(:translate)
        define_method(:translate) do |*args|
            InplaceTrans.translate(old_translate, *args)
        end
        alias :t :translate
    end)
end
medio bit
fuente
1

Se reduce a sesgo personal, prefiero las llaves en lugar de un bloque do / end, ya que es más comprensible para un mayor número de desarrolladores debido a que la mayoría de los lenguajes de fondo los usan en la convención do / end. Dicho esto, la clave real es llegar a un acuerdo dentro de su tienda, si do / end es utilizado por los desarrolladores del 6/10 que TODOS deberían usarlos, si el 6/10 usa llaves, entonces mantén ese paradigma.

Se trata de hacer un patrón para que el equipo en su conjunto pueda identificar las estructuras de código más rápido.

Jake Kalstad
fuente
3
Es más que preferencia. La unión entre los dos es diferente y puede causar problemas difíciles de diagnosticar. Varias de las respuestas detallan el problema.
The Tin Man
44
En términos de personas de otros idiomas, creo que la diferencia en "comprensibilidad" es tan pequeña en este caso que no merece consideración. (Ese no fue mi voto negativo por cierto.)
Kelvin
1

Hay una sutil diferencia entre ellos, pero {} se une más fuerte que do / end.

Shankar Raju
fuente
0

Mi estilo personal es hacer hincapié en la lectura más rígidas reglas de {... }vs do... endelección, cuando tal opción es posible. Mi idea de legibilidad es la siguiente:

[ 1, 2, 3 ].map { |e| e + 1 }      # preferred
[ 1, 2, 3 ].map do |e| e + 1 end   # acceptable

[ 1, 2, 3 ].each_with_object [] do |e, o| o << e + 1 end # preferred, reads like a sentence
[ 1, 2, 3 ].each_with_object( [] ) { |e, o| o << e + 1 } # parens make it less readable

Foo = Module.new do     # preferred for a multiline block, other things being equal
  include Comparable
end

Foo = Module.new {      # less preferred
  include Comparable
}

Foo = Module.new { include Comparable }      # preferred for a oneliner
Foo = module.new do include Comparable end   # imo less readable for a oneliner

[ [ 1 ], [ 1, 2 ] ].map { |e| e.map do |e| e + 1 end }  # slightly better
[ [ 1 ], [ 1, 2 ] ].map { |e| e.map { |e| e + 1 } }     # slightly worse

En sintaxis más compleja, como bloques anidados multilínea, trato de intercalar {... }y do... enddelimitadores para obtener el resultado más natural, por ejemplo.

Foo = Module.new { 
  if true then
    Bar = Module.new {                          # I intersperse {} and keyword delimiters
      def quux
        "quux".tap do |string|                  # I choose not to intersperse here, because
          puts "(#{string.size} characters)"    # for multiline tap, do ... end in this
        end                                     # case still loks more readable to me.
      end
    }
  end
}

Aunque la falta de reglas rígidas puede producir elecciones ligeramente diferentes para diferentes programadores, creo que la optimización caso por caso para la legibilidad, aunque subjetiva, es una ganancia neta sobre el cumplimiento de las reglas rígidas.

Boris Stitnicky
fuente
1
Viniendo de Python, tal vez debería omitir Ruby y aprender Wisp en su lugar.
Cees Timmerman
Solía ​​creer que Rubyera el mejor lenguaje pragmático que existe. Debería decir lenguaje de scripting. Hoy, creo que estarías igualmente bien aprendiendo Brainfuck.
Boris Stitnicky
0

Tomé un ejemplo de la respuesta más votada aquí. Decir,

[1,2,3].map do 
  something 
end

Si verifica qué implementación interna en array.rb, el encabezado del método de mapa dice :

Invokes the given block once for each element of self.

def map(*several_variants)
    (yield to_enum.next).to_enum.to_a
end

es decir, acepta un bloque de código; todo lo que se encuentre entre do y end se ejecuta como rendimiento. Luego, el resultado se volverá a recopilar como matriz, por lo que devolverá un objeto completamente nuevo.

Por lo tanto, cada vez que encuentre un bloque do-end o {} , solo tenga un mapa mental de que el bloque de código se pasa como un parámetro, que se ejecutará internamente.

Vasanth Saminathan
fuente
-3

Hay una tercera opción: escribir un preprocesador para inferir "fin" en su propia línea, a partir de la sangría. Los pensadores profundos que prefieren un código conciso tienen razón.

Mejor aún, piratea Ruby para que sea una bandera

Por supuesto, la solución más simple de "elige tus peleas" es adoptar la convención de estilo de que una secuencia de extremos aparece en la misma línea, y enseñarle a colorear tu sintaxis para silenciarlos. Para facilitar la edición, uno podría usar scripts de editor para expandir / contraer estas secuencias.

Del 20% al 25% de mis líneas de código rubí son "fin" en su propia línea, todo inferido trivialmente por mis convenciones de sangría. Ruby es el lenguaje similar al lisp que ha logrado el mayor éxito. Cuando la gente discute esto, preguntando dónde están todos los paréntesis espantosos, les muestro una función de rubí que termina en siete líneas de "final" superfluo.

Codifiqué durante años usando un preprocesador lisp para inferir la mayoría de los paréntesis: una barra '|' abrió un grupo que se cerró automáticamente al final de la línea, y un signo de dólar '$' sirvió como marcador de posición vacío donde, de lo contrario, no habría símbolos para ayudar a inferir las agrupaciones. Esto es, por supuesto, territorio de guerra religiosa. Lisp / esquema sin paréntesis es el más poético de todos los idiomas. Aún así, es más fácil simplemente silenciar los paréntesis usando el color de sintaxis.

Todavía codifico con un preprocesador para Haskell, para agregar heredocs y para predeterminar todas las líneas de vaciado como comentarios, todo sangrado como código. No me gustan los caracteres de comentarios, sea cual sea el idioma.

Syzygies
fuente