Ruby's Enumerable#zip создает внутренние массивы?

В 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 ответ

Решение

Я буду использовать 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это внутренняя ссылка для Rubyeachметод итератора. И в 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 оставлен читателю в качестве упражнения.

Другие вопросы по тегам