Деструктурированная итерация по переменным аргументам, таким как последовательность кортежей в D

#foreach #tuples #d #variadic

Вопрос:

Допустим, я хочу обработать переменную функцию, которая поочередно получает начальные и конечные значения 1 или более интервалов, и она должна возвращать диапазон случайных значений в этих интервалах. Вы можете представить, что входные данные представляют собой сплющенную последовательность кортежей, все элементы которых распределены по одному диапазону.

 import std.meta; //variadic template predicates
import std.traits : isFloatingPoint;
import std.range;

auto randomIntervals(T = U[0], U...)(U intervals)
if (U.length/2 > 0 amp;amp; isFloatingPoint!T amp;amp; NoDuplicates!U.length == 1) {
    import std.random : uniform01;
    
    T[U.length/2] randomValues;
    // split and iterate over subranges of size 2
    foreach(i, T start, T end; intervals.chunks(2)) {   //= intervals.slide(2,2)
        randomValues[i] = uniform01 * (end - start)   start,
    }
    return randomValues.dup;
}
 

Пример не важен, я использую его только для объяснения. Размер фрагмента может быть любым конечным положительным size_t , а не только 2 , и изменение размера фрагмента должно требовать только изменения количества переменных цикла в цикле foreach.

В приведенной выше форме он не будет компилироваться, так как будет ожидать только один аргумент (диапазон) для цикла foreach. То, что я хотел бы,-это то, что скорее автоматически использует или выводит скользящее окно в виде кортежа, полученного из числа заданных переменных цикла, и заполняет дополнительные переменные следующими элементами диапазона/массива при необходимости позволяет дополнительный индекс. Согласно документации, ряд кортежей позволяет уничтожатьпреобразование элементов кортежа в переменные цикла foreach, поэтому первое, о чем я подумал,-это превращение диапазона в последовательность кортежей, но не нашел для этого удобной функции.

Есть ли простой способ перебирать деструктурированные поддиапазоны (с такой простотой, как показано в моем примере кода) вместе с индексом? Или существует функция (стандартная библиотека), которая выполняет эту работу по разбиению диапазона на перечислимые кортежи одинакового размера? Как легко превратить диапазон поддиапазонов в диапазон кортежей?

Возможно ли std.algorithm.iteration.map в этом случае (ПРАВКА: с простым аргументом функции для сопоставления и без доступа к элементам кортежа)?

ПРАВКА: Я хочу проигнорировать последний фрагмент, который не вписывается во весь кортеж. Это просто не повторяется снова и снова.

РЕДАКТИРОВАТЬ: Дело не в том, что я не мог запрограммировать это сам, я надеюсь только на простую нотацию, потому что этот вариант использования циклического перебора нескольких элементов весьма полезен. Если в D есть что-то вроде оператора «распространение» или «отдых», как в JavaScript, пожалуйста, дайте мне знать!

Спасибо.

Ответ №1:

(Добавлен в качестве отдельного ответа, потому что он значительно отличается от моего предыдущего ответа и не поместился бы в комментарии)

После прочтения ваших комментариев и обсуждения ответов до сих пор мне кажется, что вы ищете что-то вроде приведенной ниже staticChunks функции:

 
unittest {
    import std.range : enumerate;
    size_t index = 0;
    foreach (i, a, b, c; [1,2,3,1,2,3].staticChunks!3.enumerate) {
        assert(a == 1);
        assert(b == 2);
        assert(c == 3);
        assert(i == index);
          index;
    }
}

import std.range : isInputRange;

auto staticChunks(size_t n, R)(R r) if (isInputRange!R) {
    import std.range : chunks;
    import std.algorithm : map, filter;
    return r.chunks(n).filter!(a => a.length == n).map!(a => a.tuplify!n);
}


auto tuplify(size_t n, R)(R r) if (isInputRange!R) {
    import std.meta : Repeat;
    import std.range : ElementType;
    import std.typecons : Tuple;
    import std.array : front, popFront, empty;
    
    Tuple!(Repeat!(n, ElementType!R)) resu<

    static foreach (i; 0..n) {
        result[i] = r.front;
        r.popFront();
    }
    assert(r.empty);

    return resu<
}
 

Обратите внимание, что это также связано с тем, что последний кусок имеет другой размер, хотя бы потому, что он молча выбрасывается. Если такое поведение нежелательно, удалите filter его и разберитесь с ним внутри tuplify (или не делайте этого и наблюдайте за появлением исключений).

Комментарии:

1. Приятно, я как раз собирался опубликовать свое собственное решение этого вопроса, но сначала я принимаю ваш ответ 🙂 . Два часа назад я придумал, по сути, очень похожий подход.

Ответ №2:

chunks и slide возвращайте Range s, а не кортежи. Их последний элемент может содержать меньше указанного размера, в то время как кортежи имеют фиксированный размер во время компиляции.

Если вам нужно деструктурирование, вы должны реализовать свои собственные фрагменты/слайды, которые возвращают кортежи. Чтобы явно добавить индекс в кортеж, используйте enumerate . Вот пример:

 import std.typecons, std.stdio, std.range;

Tuple!(int, int)[] pairs(){
    return [
        tuple(1, 3),
        tuple(2, 4),
        tuple(3, 5)
    ];
}

void main(){
    foreach(size_t i, int start, int end; pairs.enumerate){
        writeln(i, ' ', start, ' ', end);
    }
}
 

Редактировать:

Как сказал БиоТроник, использование map также возможно:

 foreach(i, start, end; intervals
                       .chunks(2)
                       .map!(a => tuple(a[0], a[1]))
                       .enumerate){
 

Комментарии:

1. 1 из-за упоминания уменьшенного размера последнего фрагмента. Это ближе к ответу, который я хотел получить. Я надеялся на одну стандартную библиотечную функцию или умный метод (не обязательно в смысле ООП), который выполняет эту схему «chunks().map().enumerate()» за один вызов функции, точно так же, как вы можете добавить индекс при циклическом переборе простых итераций с помощью foreach. Я добавлю более подробную информацию к своему вопросу.

2. И я полагаю, что ваш пример «карты» потерпит неудачу, если intervals диапазон содержит нечетное количество элементов.

3. Для вашей проблемы не существует «простого» решения с одной функцией, так как комбинация операций (особенно с «отбросьте последнюю, если она не подходит») очень специфична и, следовательно, не подходит для стандартной библиотеки. Если вы ожидаете список интервалов нечетной длины и хотите удалить оставшуюся часть, вы можете нарезать перед группировкой: intervals[0 .. size_t(intervals.length/2)*2]

4. Спасибо, идея нарезки хороша! Однако, с моей точки зрения (по крайней мере, при преобразовании диапазона в последовательность групп статического размера), я действительно ожидал бы, что реализация будет отсекать слишком маленькие группы. Это только до тех пор, пока группы имеют статический размер , но преобразование диапазона в группы динамического размера-это совсем другая история, в которой я бы согласился с вами.

Ответ №3:

Ваш вопрос меня немного смутил, так что извините, если я неправильно понял. По сути, вы спрашиваете foreach(a, b; [1,2,3,4].chunks(2)) , может ли это сработать, верно?

Простое решение здесь состоит в том, чтобы, как вы говорите, map от куска к tuple :

 import std.typecons : tuple;
import std.algorithm : map;
import std.range : chunks;
import std.stdio : writeln;

unittest {
    pragma(msg, typeof([1,2].chunks(2).front));

    foreach(a, b; [1,2,3,4].chunks(2).map!(a => tuple(a[0], a[1]))) {
        writeln(a, ", ", b);
    }
}
 

Комментарии:

1. Ну, это «простое» решение, но для него потребуются изменения лямбды для «карты» при изменении размера фрагмента. Он не предоставляет индекс для цикла foreach и требует, чтобы вы вручную индексировали каждый элемент кортежа. Избегание явного индексированного доступа-главная цель моего вопроса. В вашем примере я лично считаю, что лучше разместить[0] и[i] в теле цикла, а не в вызове «карта».

Ответ №4:

В то же время с BioTronic я попытался закодировать какое-то собственное решение этой проблемы (протестировано на DMD). Мое решение работает для срезов (НО НЕ массивов фиксированного размера) и позволяет избежать вызова filter :

 import std.range : chunks, isInputRange, enumerate;
import std.range : isRandomAccessRange; //changed from "hasSlicing" to "isRandomAccessRange" thanks to BioTronics
import std.traits : isIterable;

/** turns chunks into tuples */
template byTuples(size_t N, M)
if (isRandomAccessRange!M) {   //EDITED
    import std.meta : Repeat;
    import std.typecons : Tuple;
    import std.traits : ForeachType;

    alias VariableGroup = Tuple!(Repeat!(N, ForeachType!M));    //Tuple of N repititions of M's Foreach-iterated Type
    /** turns N consecutive array elements into a Variable Group */
    auto toTuple (Chunk)(Chunk subArray) @nogc @safe pure nothrow
    if (isInputRange!Chunk) {       //Chunk must be indexable
        VariableGroup nextLoopVariables;    //fill the tuple with static foreach loop
        static foreach(index; 0 .. N) {
            static if ( isRandomAccessRange!Chunk ) {  // add cases for other ranges here
                nextLoopVariables[index] = subArray[index];
            } else {
                nextLoopVariables[index] = subArray.popFront();
            }
        }
        return nextLoopVariables;
    }
    /** returns a range of VariableGroups */
    auto byTuples(M array) @safe pure nothrow {
        import std.algorithm.iteration : map;

        static if(!isInputRange!M) {
            static assert(0, "Cannot call map() on fixed-size array.");
//          auto varGroups = array[].chunks(N);     //fixed-size arrays aren't slices by default and cannot be treated like ranges
            //WARNING! invoking "map" on a chunk range from fixed-size array will fail and access wrong memory with no warning or exception despite @safe!
        } else {
            auto varGroups = array.chunks(N);
        }
        //remove last group if incomplete
        if (varGroups.back.length < N) varGroups.popBack();
        //NOTE! I don't know why but `map!toTuple` DOES NOT COMPILE! And will cause a template compilation mess.
        return varGroups.map!(chunk => toTuple(chunk));     //don't know if it uses GC
    }
}

void main() {
    testArrayToTuples([1, 3, 2, 4, 5, 7, 9]);
}

// Order of template parameters is relevant.
// You must define parameters implicitly at first to be associated with a template specialization
void testArrayToTuples(U : V[], V)(U arr) {

    double[] randomNumbers = new double[arr.length / 2];
    // generate random numbers
    foreach(i, double x, double y; byTuples!2(arr).enumerate ) {    //cannot use UFCS with "byTuples"
        import std.random : uniform01;
        randomNumbers[i] = (uniform01 * (y - x)   x);
    }
    foreach(n; randomNumbers) { //'n' apparently works despite shadowing a template parameter
        import std.stdio : writeln;
        writeln(n);
    }
}
 

Использование поэлементных операций с оператором среза здесь не сработало бы, поскольку uniform01 in uniform01 * (ends[] - starts[]) starts[] вызывался бы только один раз, а не несколько раз.

РЕДАКТИРОВАТЬ: Я также протестировал некоторые онлайн-компиляторы для D для этого кода, и странно, что они ведут себя по-разному для одного и того же кода. Для составления D я могу порекомендовать

  • https://run.dlang.io/ (Я был бы очень удивлен, если бы это не сработало)
  • https://www.mycompiler.io/new/d (но немного медленно)
  • https://ideone.com (это работает, но делает ваш код общедоступным! Не используйте с защищенным кодом.)

но это не сработало для меня:

  • https://tio.run/#d2 (в одном случае компиляция не была завершена, в противном случае неправильные результаты при выполнении даже при использовании динамического массива для теста)
  • https://www.tutorialspoint.com/compile_d_online.php (не компилирует статический foreach)

Комментарии:

1. С этим решением есть некоторые проблемы, которые я собираюсь отнести скорее к языку, чем к вам. В byTuples функции varGroups переменная сохраняется в стеке, и когда функция возвращает ссылку, она становится недействительной. Это может привести к повреждению памяти в какой-то момент позже. Скомпилируйте с -dip1000 (который, я полагаю, скоро будет по умолчанию), чтобы получить там ошибку.

2. Вы также используете эту hasSlicing черту, чтобы проверить, можно ли Chunk ее индексировать. Как следует из названия, hasSlicing проверьте, является ли тип срезаемым ( foo[1..3] ), одновременно hasIndexing проверяя индексируемость ( foo[2] ). Я не знаю ни о каких типах, которые имеют нарезку, но не индексирование, но это теоретически возможно. Это не большая проблема.

3. @BioTronic О, хорошо. Я этого не знал. Я тоже еще немного новичок, и мне требуется много времени, чтобы найти библиотечные символы, которые делают то, что и как я хотел бы иметь.

4. Кстати, я не смог найти hasIndexing на Фобосе, но вместо isRandomAccessRange этого используется для проверки индексируемого диапазона.

5. Вы абсолютно правы, isRandomAccessRange это то, о чем я думал. У меня нет оправдания. :p