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 оставлен читателю в качестве упражнения.