2010-11-04 11 views
13

Estaba leyendo un artículo sobre meta-programación y mostró que puede definir un método dentro de otro método. Esto es algo que he sabido por un tiempo, pero me hizo preguntarme: ¿tiene esto alguna aplicación práctica? ¿Hay algún uso en la vida real de definir un método dentro de un método?Ruby: ¿Definir un método dentro de otro método tiene algún uso real?

Ex:

def outer_method 
    def inner_method 
    # ... 
    end 
    # ... 
end 
+0

pequeña comunidad de desarrolladores de rubíes aquí supongo. – zengr

+5

¿Sabe que una vez que llama a 'outer_method', cualquiera puede llamar a' inner_method'? –

Respuesta

11

Mi ejemplo de metaprogramación favorito como este está construyendo dinámicamente un método que luego va a usar en un bucle. Por ejemplo, tengo un motor de consultas que escribí en Ruby, y una de sus operaciones es el filtrado. Hay un montón de diferentes formas de filtros (subcadena, igual, < =,> =, intersecciones, etc.). El enfoque ingenuo es así:

def process_filter(working_set,filter_type,filter_value) 
    working_set.select do |item| 
    case filter_spec 
     when "substring" 
     item.include?(filter_value) 
     when "equals" 
     item == filter_value 
     when "<=" 
     item <= filter_value 
     ... 
    end 
    end 
end 

Pero si sus conjuntos de trabajo pueden obtener grandes, que está haciendo esta afirmación gran caso 1000s o 1000000s de veces para cada operación a pesar de que va a tomar la misma rama en cada iteración. En mi caso, la lógica es mucho más complicada que solo una declaración de caso, por lo que la sobrecarga es aún peor. En su lugar, puede hacerlo de esta manera:

def process_filter(working_set,filter_type,filter_value) 
    case filter_spec 
    when "substring" 
     def do_filter(item,filter_value) 
     item.include?(filter_value) 
     end 
    when "equals" 
     def do_filter(item,filter_value) 
     item == filter_value 
     end 
    when "<=" 
     def do_filter(item,filter_value) 
     item <= filter_value 
     end 
    ... 
    end 
    working_set.select {|item| do_filter(item,filter_value)} 
end 

Ahora la ramificación de una sola vez se hace una vez, desde el principio, y la función de una sola función resultante es la que se utiliza en el bucle interno.

De hecho, mi ejemplo real tiene tres niveles de esto, ya que hay variaciones en la interpretación del conjunto de trabajo y el valor del filtro, no solo la forma de la prueba real. Así que construyo una función de preparación de elementos y una función filter-value-prep, y luego construyo una función do_filter que los use.

(Y utilizo realmente lambdas, no DEFS.)

+0

¡Gran ejemplo, muchas gracias! – agentbanks217

+6

Como se señaló al final, este es un gran caso para * lambdas *, no para definir un método. Definir un nuevo método en la clase para este propósito es una exageración seria. – Chuck

+3

¿Puedo preguntar por qué es una 'exageración seria'? A mi entender, es solo una diferencia de hacer que el método sea una variable, ¿verdad? – lulalala

5

Sí, los hay. De hecho, apostaría a que usa al menos un método que define otro método todos los días: attr_accessor. Si utiliza Rails, hay una tonelada más en uso constante, como belongs_to y has_many. También es generalmente útil para construcciones de estilo AOP.

+0

Estos métodos definen otros métodos, pero sus implementaciones no se ven como el ejemplo en la pregunta. Estos métodos son * class * métodos que usan 'define_method' para definir * instancia * métodos. Si usaban 'def', definirían más métodos de * clase *, lo que no sería útil. Tampoco podían tomar el nombre del nuevo método como argumento, porque 'def' no es un método que toma un argumento, es un constructo sintáctico que necesita un nombre literal en el código fuente. – Peeja

+0

(En realidad, una ligera corrección: en algunos casos, las macros como estas no usan 'define_method', sino que construyen una cadena que contiene una construcción' def' y 'class_eval' la cadena. Pero eso no es lo mismo que simplemente anidar una 'def' dentro de' def'.) – Peeja

+0

(No noté que la pregunta técnicamente no se trata de 'def's en' def's específicamente, sino de definir métodos de otros métodos. Lo que has dicho es absolutamente correcto, simplemente no deberías hacerlo de la manera en que OP ilustró en su ejemplo.) – Peeja

0

Estaba pensando en una situación recurrente, pero no creo que tendría bastante sentido.

5

Creo que hay otra ventaja de usar métodos internos, que es la claridad. Piénselo: una clase con una lista de métodos es una lista de métodos plana y no estructurada. Si te preocupa la separación de las preocupaciones y mantener las cosas en el mismo nivel de abstracción Y la pieza de código se usa solo en un lugar, los métodos internos ayudan a la vez que insinúan que solo se usan en el método de encierre.

Suponga que tiene este método en una clase:

class Scoring 
    # other code 
    def score(dice) 
    same, rest = split_dice(dice) 

    set_score = if same.empty? 
     0 
    else 
     die = same.keys.first 
     case die 
     when 1 
     1000 
     else 
     100 * die 
     end 
    end 
    set_score + rest.map { |die, count| count * single_die_score(die) }.sum 
    end 

    # other code 
end 

Ahora, eso es una especie de simple transformación estructura de datos y más código de nivel superior, la adición de puntuación de los dados que forman un conjunto y los que no pertenecen a el conjunto. Pero no está muy claro qué está pasando. Vamos a hacerlo más descriptivo. Una refactorización sencilla sigue:

class Scoring 
    # other methods... 
    def score(dice) 
    same, rest = split_dice(dice) 

    set_score = same.empty? ? 0 : get_set_score(same) 
    set_score + get_rest_score(rest) 
    end 

    def get_set_score(dice) 
    die = dice.keys.first 
    case die 
    when 1 
     1000 
    else 
     100 * die 
    end 
    end 

    def get_rest_score(dice) 
    dice.map { |die, count| count * single_die_score(die) }.sum 
    end 

    # other code... 
end 

idea de get_set_score() y get_rest_score() es un documento en el uso de un descriptiva (aunque no es muy buena en este ejemplo inventado) lo hacen los animales divididos.Pero si tienes muchos métodos como este, el código en la puntuación() no es tan fácil de seguir, y si refactorizas cualquiera de los métodos, es posible que necesites comprobar qué otros métodos usan (incluso si son privados, otros métodos de la misma clase podrían usarlos).

En cambio, estoy empezando a preferir esto:

class Scoring 
    # other code 
    def score(dice) 
    def get_set_score(dice) 
     die = dice.keys.first 
     case die 
     when 1 
     1000 
     else 
     100 * die 
     end 
    end 

    def get_rest_score(dice) 
     dice.map { |die, count| count * single_die_score(die) }.sum 
    end 

    same, rest = split_dice(dice) 

    set_score = same.empty? ? 0 : get_set_score(same) 
    set_score + get_rest_score(rest) 
    end 

    # other code 
end 

En este caso, debería ser más obvio que get_rest_score() y get_set_score() se envuelven en métodos para mantener la lógica de la puntuación() en el propio mismo nivel de abstracción, sin intromisión con los hashes etc.

Tenga en cuenta que técnicamente se puede llamada alcanzaron el # get_set_score y alcanzaron el # get_rest_score, pero en este caso se trataría de un mal estilo de la OMI, ya que son semánticamente métodos simplemente privadas para la puntaje de método único()

Por lo tanto, al tener esta estructura siempre puede leer toda la implementación de la puntuación() sin buscar ningún otro método definido fuera del puntaje # de Puntuación. Aunque no veo ese código de Ruby a menudo, creo que voy a convertir más en este estilo estructurado con métodos internos.

NOTA: Otra opción que no se ve tan limpia, pero evita el problema de los conflictos de nombres es simplemente utilizar lambdas, que ha existido en Ruby desde el primer momento. Siguiendo con el ejemplo, se convertiría en

get_rest_score = -> (dice) do 
    dice.map { |die, count| count * single_die_score(die) }.sum 
end 
... 
set_score + get_rest_score.call(rest) 

Se ISN tan bonita - alguien que busca en el código podría preguntarse por qué todos estos lambdas, mientras que el uso de métodos internos es bastante auto-documentado. Todavía me inclinaría más hacia las lambdas, ya que no tienen el problema de filtrar nombres potencialmente conflictivos al alcance actual.

+0

me gusta preferir la misma práctica. definiendo métodos dentro de un método para simplificar el código y la claridad. es muy útil cuando su método se vuelve pesado (complejidad) y no desea distinguir el método_externo para su llamada. – ajahongir

+0

Esto no es una buena idea. Ruby no nota el 'def' dentro de' # score' y define el método * una vez *, lo define cada vez (y solo cuando) '# score' se ejecuta.Eso significa que '# get_set_score' no existe hasta que se llame a' # score' * y * se redefina cada vez que se llame a '# get_set_score'. No solo es extraño, también invalida la caché global de métodos de Ruby, lo que ralentizará su programa de forma espectacular. – Peeja

+0

Totalmente de acuerdo con Peeja, por lo que a menos que el lenguaje sea compatible con métodos internos reales, es probable que no sea prudente usarlos (¿quizás Ruby debería emitir una advertencia si detecta tal o incluso un error?) – EdvardM

3

No se usa def. No existe una aplicación práctica para eso, y el compilador probablemente debería generar un error.

Existen razones para definir un método dinámicamente durante el curso de la ejecución de otro método. Considere attr_reader, que se implementa en C, pero podría ser implementado de manera equivalente en Ruby como:

class Module 
    def attr_reader(name) 
    define_method(name) do 
     instance_variable_get("@#{name}") 
    end 
    end 
end 

Aquí, nosotros usamos #define_method para definir el método. #define_method es un método real; def no es. Eso nos da dos propiedades importantes. Primero, toma un argumento, lo que nos permite pasarle la variable name para nombrar el método. En segundo lugar, se necesita un bloque, que cierra nuestra variable name, lo que nos permite usarlo desde dentro de la definición del método.

Entonces, ¿qué ocurre si utilizamos def en su lugar?

class Module 
    def attr_reader(name) 
    def name 
     instance_variable_get("@#{name}") 
    end 
    end 
end 

Esto no funciona en absoluto. En primer lugar, la palabra clave def va seguida de un nombre literal, no una expresión. Eso significa que estamos definiendo un método llamado, literalmente, #name, que no es lo que queríamos en absoluto. En segundo lugar, el cuerpo del método se refiere a una variable local llamada name, pero Ruby no la reconocerá como la misma variable que el argumento #attr_reader. La construcción def no usa un bloque, por lo que ya no se cierra sobre la variable name.

La construcción def no le permite "transferir" ninguna información para parametrizar la definición del método que está definiendo. Eso lo hace inútil en un contexto dinámico. No hay ninguna razón para definir un método usando def desde dentro de un método. Siempre puedes mover la misma construcción interna def del exterior def y terminar con el mismo método.


Además, definir los métodos dinámicamente tiene un costo. Ruby almacena en caché las ubicaciones de memoria de los métodos, lo que mejora el rendimiento. Cuando agrega o elimina un método de una clase, Ruby tiene que descartar ese caché. (Antes de Ruby 2.1, que caché era mundial. A partir de 2.1, la memoria caché es por clase.)

Si se define un método dentro de otro método, cada vez que el método externa se llama, se invalida la memoria caché. Eso está bien para las macros de nivel superior como attr_reader y belongs_to de Rails, porque todos se llaman cuando el programa se está iniciando y luego (con suerte) nunca más. La definición de métodos durante la ejecución continua de su programa lo ralentizará un poco.

Cuestiones relacionadas