2011-06-27 10 views
7

En Ruby - Compare two Enumerators elegantly, se dijo¿Ruby's Enumerable # zip crea matrices internamente?

El problema con cremallera es que crea matrices internamente, no importa lo que Enumerable que pase. Hay otro problema con longitud de entrada params

tuve un vistazo a la implementación de # postal Enumerable en YARV, y vio

static VALUE 
enum_zip(int argc, VALUE *argv, VALUE obj) 
{ 
    int i; 
    ID conv; 
    NODE *memo; 
    VALUE result = Qnil; 
    VALUE args = rb_ary_new4(argc, argv); 
    int allary = TRUE; 

    argv = RARRAY_PTR(args); 
    for (i=0; i<argc; i++) { 
     VALUE ary = rb_check_array_type(argv[i]); 
     if (NIL_P(ary)) { 
      allary = FALSE; 
      break; 
     } 
     argv[i] = ary; 
    } 
    if (!allary) { 
     CONST_ID(conv, "to_enum"); 
     for (i=0; i<argc; i++) { 
      argv[i] = rb_funcall(argv[i], conv, 1, ID2SYM(id_each)); 
     } 
    } 
    if (!rb_block_given_p()) { 
     result = rb_ary_new(); 
    } 
    /* use NODE_DOT2 as memo(v, v, -) */ 
    memo = rb_node_newnode(NODE_DOT2, result, args, 0); 
    rb_block_call(obj, id_each, 0, 0, allary ? zip_ary : zip_i, (VALUE)memo); 

    return result; 
} 

Estoy entendiendo los siguientes bits correctamente?

Compruebe si todos los argumentos son matrices, y si es así, reemplace alguna referencia indirecta a la matriz con una referencia directa

for (i=0; i<argc; i++) { 
     VALUE ary = rb_check_array_type(argv[i]); 
     if (NIL_P(ary)) { 
      allary = FALSE; 
      break; 
     } 
     argv[i] = ary; 
    } 

Si no lo son todas las matrices, crear un enumerador lugar

if (!allary) { 
     CONST_ID(conv, "to_enum"); 
     for (i=0; i<argc; i++) { 
      argv[i] = rb_funcall(argv[i], conv, 1, ID2SYM(id_each)); 
     } 
    } 

Crear una matriz de matrices sólo si un bloque no se da

if (!rb_block_given_p()) { 
     result = rb_ary_new(); 
    } 

Si todo es una matriz, utilizar zip_ary, de lo contrario usar zip_i, y llamar a un bloque en cada conjunto de valores

/* use NODE_DOT2 as memo(v, v, -) */ 
    memo = rb_node_newnode(NODE_DOT2, result, args, 0); 
    rb_block_call(obj, id_each, 0, 0, allary ? zip_ary : zip_i, (VALUE)memo); 

devolver una matriz de matrices si no se da bloque, nula rentabilidad más (Qnil)?

return result; 
} 

Respuesta

6

Voy a utilizar 1.9.2-p0, ya que es lo que tengo a mano.

La función rb_check_array_type se parece a esto:

VALUE 
rb_check_array_type(VALUE ary) 
{ 
    return rb_check_convert_type(ary, T_ARRAY, "Array", "to_ary"); 
} 

Y rb_check_convert_type se parece a esto:

VALUE 
rb_check_convert_type(VALUE val, int type, const char *tname, const char *method) 
{ 
    VALUE v; 

    /* always convert T_DATA */ 
    if (TYPE(val) == type && type != T_DATA) return val; 
    v = convert_type(val, tname, method, FALSE); 
    if (NIL_P(v)) return Qnil; 
    if (TYPE(v) != type) { 
     const char *cname = rb_obj_classname(val); 
     rb_raise(rb_eTypeError, "can't convert %s to %s (%s#%s gives %s)", 
       cname, tname, cname, method, rb_obj_classname(v)); 
    } 
    return v; 
} 

Nota convert_type la llamada. Esto se parece mucho a la versión C de Array.try_convert y try_convert sólo pasa a tener este aspecto:

/* 
* call-seq: 
*  Array.try_convert(obj) -> array or nil 
* 
* Try to convert <i>obj</i> into an array, using +to_ary+ method. 
* Returns converted array or +nil+ if <i>obj</i> cannot be converted 
* for any reason. This method can be used to check if an argument is an 
* array. 
* 
*  Array.try_convert([1]) #=> [1] 
*  Array.try_convert("1") #=> nil 
* 
*  if tmp = Array.try_convert(arg) 
*  # the argument is an array 
*  elsif tmp = String.try_convert(arg) 
*  # the argument is a string 
*  end 
* 
*/ 
static VALUE 
rb_ary_s_try_convert(VALUE dummy, VALUE ary) 
{ 
    return rb_check_array_type(ary); 
} 

Así que, sí, el primer bucle está buscando cualquier cosa en argv que no es una matriz y el establecimiento del indicador allary si encuentra tal cosa

En enum.c, vemos esto:

id_each = rb_intern("each"); 

Así id_each es una referencia interna para el método iterador Rubí each.Y en vm_eval.c, tenemos esto:

/*! 
* Calls a method 
* \param recv receiver of the method 
* \param mid an ID that represents the name of the method 
* \param n  the number of arguments 
* \param ... arbitrary number of method arguments 
* 
* \pre each of arguments after \a n must be a VALUE. 
*/ 
VALUE 
rb_funcall(VALUE recv, ID mid, int n, ...) 

Así que esto:

argv[i] = rb_funcall(argv[i], conv, 1, ID2SYM(id_each)); 

está llamando to_enum (con, esencialmente, la default argument) sobre lo que está en argv[i].

Por lo tanto, el resultado final de los primeros bloques for y if es que argv está lleno de matrices o lleno de enumeradores en lugar de ser una mezcla de los dos. Pero observe cómo funciona la lógica: si se encuentra algo que no es una matriz, todo se convierte en un enumerador. La primera parte de la función enum_zip envolverá matrices en enumeradores (que es esencialmente gratuita o al menos lo suficientemente barata como para no preocuparse) pero no expandirá los enumeradores en matrices (lo que podría ser bastante caro). Las versiones anteriores podrían haber ido para otro lado (prefieren matrices sobre enumeradores), lo dejo como un ejercicio para el lector o los historiadores.

La parte siguiente:

if (!rb_block_given_p()) { 
    result = rb_ary_new(); 
} 

crea una matriz nueva vacío y lo deja en result si zip se está llamando sin un bloque. Y aquí hay que señalar lo zip returns:

enum.zip(arg, ...) → an_array_of_array 
enum.zip(arg, ...) {|arr| block } → nil 

Si hay un bloque, entonces no hay nada para volver y result puede permanecer como Qnil; si no hay un bloque, entonces necesitamos una matriz en result para que se pueda devolver una matriz.

De parse.c, vemos que NODE_DOT2 es un rango de puntos dobles pero parece que solo están usando el nuevo nodo como una simple estructura de tres elementos; rb_new_node simplemente asigna un objeto, establece algunos bits, y asigna tres valores en un struct:

NODE* 
rb_node_newnode(enum node_type type, VALUE a0, VALUE a1, VALUE a2) 
{ 
    NODE *n = (NODE*)rb_newobj(); 

    n->flags |= T_NODE; 
    nd_set_type(n, type); 

    n->u1.value = a0; 
    n->u2.value = a1; 
    n->u3.value = a2; 

    return n; 
} 

nd_set_type es sólo una macro poco jugando. Ahora tenemos memo como una estructura de tres elementos. Este uso de NODE_DOT2 parece ser un inconveniente conveniente.

La función rb_block_call parece ser el iterador interno central. Y vemos a nuestro amigo id_each nuevamente, así que haremos una iteración each. Entonces vemos una elección entre zip_i y zip_ary; aquí es donde las matrices internas se crean y se presionan en result. La única diferencia entre zip_i y zip_ary parece ser la gestión de excepciones StopIteration en zip_i.

En este punto nos hemos hecho la compresión y que o bien tener la matriz de matrices en result (si no había bloque) o tenemos Qnil en result (si había un bloque).


Resumen Ejecutivo: El primer bucle explícitamente Evita expansión enumeradores en matrices. Las llamadas zip_i y zip_ary solo funcionarán con matrices no temporales si tienen que crear una matriz de matrices como valor de retorno.Por lo tanto, si llama al zip con al menos un enumerador que no es de matriz y utiliza el formulario de bloque, entonces son los enumeradores hasta el final y el "problema con el zip es que crea matrices internamente" no sucede. La revisión de 1.8 u otras implementaciones de Ruby se deja como un ejercicio para el lector.

+0

"Si no hay bloque, no hay nada que devolver y el resultado puede seguir siendo Qnil", ¿quiere decir "si hay un bloque"? –

+1

@Andrew: Correcto, corregido. Estaba escribiendo esto a las 05:00 después de todo :) –

+0

¿Qué significa "memo"? ¿Es corto para la memorización? –

Cuestiones relacionadas