Ruby's Enumerable # zip создает массивы внутри? - PullRequest
7 голосов
/ 27 июня 2011

В Ruby - элегантно сравнивайте два перечислителя , как было сказано

Проблема с zip заключается в том, что он создает массивы внутри, независимо от того, какой Enumerable вы передаете.Есть другая проблема с длиной входных параметров

Я посмотрел на реализацию Enumerable # zip в YARV и увидел

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;
}

Правильно ли я понимаю следующие биты?

Проверьте, являются ли все аргументы массивами, и если да, замените некоторую косвенную ссылку на массив прямой ссылкой

    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();
    }

Если все является массивом, используйте zip_ary, в противном случае используйте zip_i, ивызывать блок для каждого набора значений

    /* 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);

Возвращать массив массивов, если блок не указан, иначе вернуть nil (Qnil)?

    return result;
}

1 Ответ

6 голосов
/ 27 июня 2011

Я буду использовать 1.9.2-p0, поскольку это то, что у меня под рукой.

Функция rb_check_array_type выглядит следующим образом:

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

И rb_check_convert_type выглядиткак это:

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;
}

Обратите внимание на вызов convert_type.Это очень похоже на C-версию Array.try_convert и try_convert просто выглядит так:

/*   
 *  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);
}

Так что, да, первый цикл ищет что-то в argv это не массив и установка флага allary, если он найдет такую ​​вещь.

В enum.c мы видим это:

id_each = rb_intern("each");

Итак, id_each - этовнутренняя ссылка на метод итератора Ruby each.И в vm_eval.c у нас есть это:

/*!  
 * 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, ...)

Итак, это:

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

Вызывает to_enum (с, по сути, аргументом по умолчанию ) для всего, что находится в argv[i].

Итак, конечный результат первых блоков for и if состоит в том, что argv либо полон массивов, либо полон перечислителей, а не может бытьсмесь двух.Но обратите внимание, как работает логика: если найдено что-то, что не является массивом, то все становится перечислителемПервая часть функции enum_zip обернет массивы в перечислителях (которые по существу бесплатны или, по крайней мере, достаточно дешевы, чтобы о них не беспокоиться), но не расширит перечислители в массивы (которые могут быть довольно дорогими).Более ранние версии могли пойти другим путем (предпочитая массивы перед перечислителями), я оставлю это как упражнение для читателя или историков.

Следующая часть:

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

Создаетновый пустой массив и оставляет его в result, если zip вызывается без блока.И здесь мы должны отметить, что zip возвращает :

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

Если есть блок, то возвращать нечего и result может оставаться как Qnil;если блока нет, то нам нужен массив в result, чтобы можно было возвращать массив.

Начиная с parse.c, мы видим, что NODE_DOT2 - это диапазон из двух точек, но онпохоже, они просто используют новый узел как простую трехэлементную структуру;rb_new_node просто выделяет объект, устанавливает несколько битов и присваивает три значения в структуре:

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 - это просто битовый макрос.Теперь у нас есть memo как структура из трех элементов.Такое использование NODE_DOT2 представляется удобным ключом.

Функция rb_block_call является внутренним итератором ядра.И мы снова видим нашего друга id_each, поэтому будем выполнять итерацию each.Затем мы видим выбор между zip_i и zip_ary;это где внутренние массивы создаются и помещаются в result.Единственная разница между zip_i и zip_ary, по-видимому, заключается в обработке исключений StopIteration в zip_i.

На данный момент мы выполнили архивирование, и у нас либо есть массив массивов в result(если не было блока) или у нас Qnil в result (если был блок).


Резюме : первый цикл явно избегает расширения перечислителейв массивы.Вызовы zip_i и zip_ary будут работать только с временными массивами, если они должны создать массив массивов в качестве возвращаемого значения.Таким образом, если вы вызываете zip хотя бы с одним перечислителем, не являющимся массивом, и используете блочную форму, то это перечислители до конца, и «проблема с zip в том, что он создает массивы внутри» не возникает.Обзор 1.8 или других реализаций Ruby оставлен читателю в качестве упражнения.

...