Comments 173
Во втором питоне был xrange (итератор). В целом, for in
— это один из элементов хорошей эргономики языков, который появился относительно поздно.
В Rust его эргономику подняли ещё выше. Не теряя общности протокола итерации, они сделали так:
for x in 0..10 {
...
}
Более того, зацените с inclusive:
for x in 0..=10 {
...
}
(досчитает до 10 включительно)
А ещё много всего вкусного: https://doc.rust-lang.org/reference/expressions/range-expr.html
Это мы еще не вспоминали (0..10).map(function)
for (point, weight) in points.iter()
.zip(weights)
{
println!("{}, {}", point.x, weight);
}
И для плюсов, невзирая на отсутствие в стандартной библиотеке он реализуется достаточно просто (сто лет кресты не использовал, так-что легкий копи-паст кода haqreu).
В целом, for in — это один из элементов хорошей эргономики языков, который появился относительно поздно.
В Rust его эргономику подняли ещё выше. Не теряя общности протокола итерации, они сделали так:
for x in 0..10
Эргномика выше по сравнению с чем? Во многих языках такой синтаксис есть, с разными мелкими отличиями типа :
вместо ..
. Да даже в питоне, который намного старше раста, выглядит похоже: for x in range(10)
, или for x in range(0, 10)
если хотите нижнюю границу указывать.
По сравнению с for (int i = 0; i < ...; i++)
Так в первом комментарии было написано мол "вот есть обычный for-in, а в расте ещё эргономичнее его сделали". Вы же сравниваете не с for-in циклом.
Ну а сам for-in-range
в питоне уже лет 20 существует, если не больше — думаю, даже это странно считать "относительно поздно" в плане развития языков программирования. Уверен, что в каких-то других языках такое и раньше было сделано.
Эргономика выше за счёт добавления range в синтаксис языка. 2..4 явно выразительнее, чем range(2,4). Букв меньше, скобок меньше.
Очень "интересная" логика. По умолчанию, если выражаться математикой цикл [0,10). В расте об эргономике вообще не думают? Обычно сахар в виде ".." подразумевает полное включение от и до
Думают, думают.
В большинстве случаев надо считать от нуля до N-1. Даже у автора поста именно этот случай описан.
Причина — если у вас есть N элементов, то у них номера от нуля до N-1.
Понятно, что математику непривычно. Но математику так же не привычно, что в компьютере нет ни натуральных, ни вещественных чисел. А ещё нельзя досчитать до бесконечности (в обоих направлениях).
Во-первых, откуда "обычно", во-вторых, на практике полувключающие диапазоны удобнее
Посмотрел таки ради интереса по вашей ссылке — эта штука с двумя точками в расте очень ограниченная, ничего не поменялось с тех пор как я с ним игрался:
- Произвольный шаг в таком синтаксисе не поддерживается, для него нужно наворачивать (0..10).stepby(2).
- Можно использовать только со стандартными целыми числами, если я правильно понял: никаких вам bigint/float/datetime/… .
- Произвольный шаг в таком синтаксисе не поддерживается, для него нужно наворачивать (0..10).stepby(2).
С учётом того, что step_by
работает не только на численных диапазонах, а на произвольных итераторах, не вижу, в чём проблема.
- Можно использовать только со стандартными целыми числами, если я правильно понял: никаких вам bigint/float/datetime/…
После стабилизации Step можно будет реализовать его для своих типов и использовать для нативных диапазонов. Если nightly не пугает, то можно и сейчас.
С учётом того, что step_by работает не только на численных диапазонах, а на произвольных итераторах, не вижу, в чём проблема.
Ну так соответствующая операция на итераторах в любом языке наверное есть, в котором итераторы используются. Даже в питоне — islice ровно это делает.
В общем, никак не понимаю чем итераторы именно в расте удобнее/эргономичнее, как тут выше писали — по-моему они совершенно обычные, возможности такие же как везде.
После стабилизации Step можно будет реализовать его для своих типов и использовать для нативных диапазонов.
О, это хорошо, рад за раст :)
Если nightly не пугает, то можно и сейчас.
Я растом по факту не пользуюсь, только игрался и посматриваю иногда на концептуальном уровне. Лично для меня он встал в категорию языков "придуманы и реализованы интересные/полезные идеи, но мне не подходит" — там же и например lisp, haskell. Мне для всяких вычислений, статистики и анализа данных (+ остальное по мелочи) главное — экосистема, которая в расте по сути отсутствует; ещё слишком многословно выглядит всё-таки, и слишком "низкоуровневый".
habr.com/ru/post/264803
Звучит как zip()
+ filter()
или даже filter_map()
. С хорошей базой итераторов можно много делов наворотить.
Вам скорее нужна правильная абстракция, позволяющая работать со структурой массивов, как с массивом структур.
Специальный контейнер, возвращающий прокси, которые берут данные из массивов по заданному смещению.
Ну да, в этом случае только zip.
Лучше не tuple, а namedtuple (есть такое в C++?), или struct — чтобы обращаться нормально по имени, типа a[i, j].cost. Не скажу за C++, но я иногда пользуюсь таким виртуальным представлением struct-of-arrays в виде array-of-structs — удобно, когда работающая с ним функция особо не заморачивается, какой из этих двух вариантов ей передали.
tuple можно легко разобрать на запчасти при помощи structural binding, так что имена будут красивыми.
Только если типы в кортеже одинаковые, то их легко перепутать
Я имел в виду какой-то такой сценарий, в котором обычно пользуюсь SoA-as-AoS. Есть функция, которая обрабатывает массив структур:
f(A) = [a.field_1 + a.field_2 for a in A]
Ещё есть функция, которая работает с обычными числовыми массивами:
g(A, B) = A * B
И нужно несколько раз по очереди их применять к данным одного и того же массива. Тогда пишу примерно так:
field_1 = ... # массив чисел
field_2 = ... # массив чисел
A_s = StructArray(field_1=field_1, field_2=field_2)
for iter in 1:niters
xxx = f(A_s)
yyy = g(A_s.field_1, A_s.field_2)
end
Тут важно, чтобы элементы A_s представлялись как структуры с правильными названиями полей, иначе f(A_s) не сработает.
Вообще очень удобно получается такая абстракция — код чище, функция может принимать один массив вместо условных пяти, которые должны быть одного размера.
for (a <- 0 to 4; b <- 1 to 5; c <- 'a' to 'c') {
println(s"$a, $b, $c")
}
output:
0, 1, a
0, 1, b
0, 1, c
0, 2, a
0, 2, b
0, 2, c
…
Я скалу не знаю, но глядя на этот синтаксис у меня есть сразу же куча интерпретаций:
- Одновременная итерация по трём итерейбл (типа
for x in zip()
) до момента пока все итерируются. - Одновременная итерация по трём итерейбл (типа
for x in zip()
) до момента пока хотя бы одна итерируется. - Три вложенных цикла (слева направо)
- Три вложенных цикла (справа налево).
Так что не очень выразительно.
for (a <- 0 to 3; b <- 1 to 4; c <- 2 to 5)
выглядит куда компактней, чемfor (int a = 0; a < 4; a++) {
for (int b = 1; a < 5; b++) {
for (int с = 2; a < 6; c++) {
По крайней мере это выглядит любопытно
В сях тоже можно при желании вложенные циклы сделать в рамках одной конструкции for(;;). Но это как с тем троллейбусом: зачем?
for {
a <- 0 to 3
b <- 1 to 4
c <- 2 to 5
} {...}
Может тогда к такому нужно стремится
for (auto [a, b, c]: range(4, 5, 6)) {
std::cerr << a << b << c;
}
Сделал на JS, получилось в 30 раз медленнее вложенных циклов
for(const [x, y, z] of matrix(100, 100, 100)) {
console.log(x, y, z)
}
function* matrix() {
const len = arguments.length,
values = Array(len).fill(0)
for(let pos = len - 1;;) {
while(values[pos] < arguments[pos]) {
yield values.slice()
values[pos]++
}
while(pos-- > 0 && values[pos] + 1 === arguments[pos]);
if(pos < 0) break
values[pos]++
while(++pos < len) values[pos] = 0
yield values.slice()
}
}
const a = 100
let time1 = -performance.now()
let res1 = 0
// for(let i = 0; i < 10000; i++)
for(const [x, y, z] of matrix(a, a, a)) {
res1 += x + y + z
}
time1 += performance.now()
let time2 = -performance.now()
let res2 = 0
// for(let i = 0; i < 10000; i++)
for(let x = 0; x < a; x++) {
for(let y = 0; y < a; y++) {
for(let z = 0; z < a; z++) {
res2 += x + y + z
}
}
}
time2 += performance.now()
console.log(res1 === res2, res1) // true 148500000
console.log('Matrix:', time1, 'ms') // "Matrix:" 925.7049999978335 "ms"
console.log('Loop:', time2, 'ms') // "Loop:" 30.43500000057975 "ms"
console.log(time1 / time2) // 30.32500790616872
Но я хочу идти ещё дальше, я хочу, чтобы самый частый цикл не имел даже упоминания о нижней границе.
Можно создать implicit класс для чисел или любого другого типа, и потом писать что-то вроде for (i<-5.times){...}
или 5 times { .... }
.
Но более привычно выглядят варианты for(i<-0 until 5){...}
и for(i<- array.indices){ ... }
.
for(int = i=0,j=0; i<5;i++,j++) — цикл один, а переменных несколько?
как не спутать с тем что в примере с вложенными.
По-хорошему такой цикл через zip делается, хоть на скале хоть на чём: for (i, j) in zip(0:4, 0:4)
с поправками на детали синтаксиса разных языков.
for {
j = 0
i <- 0 to 5
j += 1
}
Но, с большой вероятностью, за такой код в scala сообществе закидают тапками )
b < — 1 to 4;
С одной стороны, визуально кажется что там отрицательное значение -1
for (int b = 1; a < 5; b++) {
С другой стороны, возможен баг за счет копипасты
for в скала это обычная do-нотация, о чем можно легко догадаться по <-
, следовательно вспоминаем как реализован стандартный аппликатив для списков — каждый с каждым.
Для более полной демонстрации циклов scala я бы привёл пример посложнее:
for(i<-0 until 10;
k=h(i);
j<-1 to 5 if f(j,k)>5;
_ = println(s"$i $j $k");
(v,5)<-someIterable){
...
}
И это ещё без yield (который в scala довольно убогий по использованию и производительности).
/ sarcasm
В 2020 должна быть функциональщина с хвостовой рекурсией вместо цикла /
Ну а если серьезно — зависит от требований и контекста. Если не нужна оптимальная производительность, то можно действительно использовать функциональщину или что-то типа LINQ из С# аля method chaining (не знаю, завезли ли такой синтаксис в плюсы уже наконец-то). Если производительность таки важна, можно попробовать вынести циклы выше 3-го уровня вложенности в отдельную функцию, в надежде что умный компилятор все заинлайнит. Желательно вынести так чтоб это ещё и имело какой-то «физический смысл», тобишь было очевиден новичку в проекте.
Ну и да, я лично целиком поддерживаю ваше желание убрать «лишние буквы» — чем меньше букв тем проще читать код (обычно, бывают исключения).
Ложная дилемма. В расте итераторы быстрее циклов. Потому что итераторы автоматически выкидывают все проверки индексов, например.
То что LINQ в шарпе реализован через одно место не означает, что нельзя делать быстрые и эффективные итераторы.
Я в общем-то имел в виду не то что LINQ медленный, а то, что переход на функциональщину (генераторы, иммутабельность, алгебраические типы, и т.п.) может при правильном применении упростить код, но (обычно) негативно сказывается на производительности, т.к. у оптимизатора появляется сильно больше работы. Ну и синтаксис С++ не позволит эти вещи использовать «удобно» (в том же расте тут все сильно лучше). Опять же, оптимизатор, возможно, сможет перелопатить функциональщину, но это нужно проверять «отдельно» — за читабельность и удобство отладки приходится платить.
Ну а что касается LINQ в шарпе — с ним не все так плохо — за ~20% замедления код получается намного более читабельным. К тому же, LINQ в шарпе реализует генераторы, а не итераторы/ренжи — по сути, это монада List из хаскелл — многие операции LINQ позволяют работать с “бесконечными” последовательностями. Исключение составляют операции требующие полной материализации (например, OrderBy). Соответственно, LINQ реализован поверх ко-процедур, а не поверх итераторов, что и даёт накладные расходы (точнее, усложняет работу оптимизатора).
Не обязательно там сопрограммы. Простые операторы там как раз итераторами сделаны.
Я в общем-то имел в виду не то что LINQ медленный, а то, что переход на функциональщину (генераторы, иммутабельность, алгебраические типы, и т.п.) может при правильном применении упростить код, но (обычно) негативно сказывается на производительности, т.к. у оптимизатора появляется сильно больше работы.
Это как раз наоборот. При использовании ФП примитивов у оптимизиатора больше возможностей, а не меньше. Классический пример, в обычном цикле for
компилятор обязан проверять выход за границы массива. При этом при использовании foreach
на итераторе он знает, что выхода за границы не будет и можно их выкинуть. С декларативным описанием программист сообщает больше информации о том, что он хочет получить в итоге, а не как, поэтому компилятору дается больше свободы в реорганизации кода чтобы получить нужый результат.
Ну а что касается LINQ в шарпе — с ним не все так плохо — за ~20% замедления код получается намного более читабельным.
Только не 20% — а на порядок
В .NET 7 наконец это гораздо улучшили: ..NET 7 improvements
В некоторых местах автовекторизация LINQ выдаёт лучшую производительность чем цикл for.
На КПДВ узел под названием «совершенная петля» (perfection loop).
прошу ногами сильно не бить, но «совершенная петля» это «perfect loop», а «perfection loop» это «петля совершенства».
"perfection" в данном случае неоднозначно — можно перевести как "петля совершенствования", c учётом контекста статьи очень даже интересная игра слов получается.
real a(10,10,10), b(10,10,10), c(5)
integer i,j,k
forall (i=1:10, j=2:10:2, k=6:8, b(i,j,k) /= 0)
a(i,j,k) = 3 * i / b(i,j,k) + c(j/2)
end forall
Не обязательно три вложенных цикла — можно по массиву этих ячеек в линейном порядке проходить. Если проходимая структура более сложная, чем многомерный массив, то казалось бы можно один раз реализовать функцию обхода по ней с индексом, а потом эту функцию только использовать как аргумент в for-in. Или у вас такое решение по какой-то причине не проходит?
По-моему такой сценарий абсолютно типичный и не требует написания вложенных циклов по размерностям каждый раз. Один раз реализуете функцию обхода-по-индексам если её нет, и потом эту функцию используете. Я бы примерно так сделал (пример на джулии, т.к. в современном c++ почти не ориентируюсь; но код не завязан фундаментально на язык):
A = ... # наш массив
offsets = CartesianIndex.([(-1, 0), (1, 0), (0, -1), (0, 1)])
for I in CartesianIndices(A)
cnt = 1
for off in offsets
if checkbounds(Bool, A, I + off) # для примера игнорируем "соседей" вне границ массива
cnt += 1
A[I] += A[I + off]
end
end
A[I] /= cnt
end
При этом, как видно из кода, очень легко обобщить на произвольное понимание "соседей" клетки, на многомерные массивы, и т.п. — явно удобнее, чем вложенный цикл на каждую размерность массива, и захардкоженные координаты соседей.
как жить дальше именно c C++
Зачем вам этот дедушка с его неизлечимыми проблемами?
Запись вложенных циклов одной строкой — согласен, удобная фича, тоже часто пользуюсь. Но почти больно :) смотреть на повторение i,j,k трижды в вашем блоке кода: иногда попадался на опечатках в таких случаях, особенно если индексы массива из переменных цикла получаются сложнее (типа a[i + 2, j, k * 3]
), или идут в другом порядке, или надо будет потом подправить это место. Поэтому теперь стараюсь писать так (пример на julia), т.е. не повторяя много раз получение индекса массива:
for i in 1:10, j in 2:2:10, k in 6:8
I = CartesianIndex(i, j, k)
if b[I] == 0 continue end
a[I] = 3 * i / b[I] + c[j ÷ 2]
end
Мне одному кажется, что если значения можно разместить в массивах, то самое удобное оформление вложенных циклов реализовано в фортране? Например:
real a(10,10,10), b(10,10,10), c(5)
integer i,j,k
forall (i=1:10, j=2:10:2, k=6:8, b(i,j,k) /= 0)
a(i,j,k) = 3 * i / b(i,j,k) + c(j/2)
end forall
Но ведь от этого отказываются?
The FORALL construct is an obsolescent language feature in Fortran 2018.
Deleted and Obsolescent Language Features
Без forall и where, просто do concurrent.
stackoverflow.com/questions/4122099/do-fortran-95-constructs-such-as-where-forall-and-spread-generally-result-in-fa/4141572#4141572
stackoverflow.com/questions/8602596/fortran-forall-restrictions
Сложные расчёты всё равно не для gcc, а ICC/IFC, Nvidia/PGI, IBM, Cray и прочие.
Там уже есть свои образцы использования for/forall/foreach…
parallel-for.sourceforge.net/index.html
parallel-for.sourceforge.net/parallelfor.html
Пример: берёте железо Nvidia, вам дают инструменты для его правильного использования.
Можно при желании доопределить свои конструкции — в рамках выбранной связки «железо + ПО». Но потом придётся разбираться что там компилятор наделал — насколько хорошо распараллелил.
Какой смысл делать свой местечковый стандарт? NIH?
Я не уверен, что это самое удобное. Кроме того, мне нужно понять, как жить дальше именно c C++ :)
В статье был намек на соседей… это просто пример. Причем из языка, который достаточно широко используется для моделирования, и по эффективности мало кому уступает. Что позволяет писать вычислительную часть на фортране и цеплять эти функции к проектам на других языках. Но у С++ эффективность сравнима с фортраном (если сравнивать хорошие компиляторы), поэтому переписывать уже имеющийся проект смысла нет, разумеется. Особенно с учетом отсутствия разработчиков, владеющих этим хорошим, но очень уж узконишевым языком…
А вопрос удобства всегда имеет субъективный оттенок, естественно. Но массивы и их сечения в современном фортране действительно очень мощные. А элементы ООП, которые в него тоже недавно (лет 20 назад) добавили, позволяют делать большие проекты с хорошо читаемым кодом. Если, конечно, там нет унаследованных из древности простыней на фортране-66 с вычисляемыми GOTO и прочими подобными ужасами ;-). Которые хотя и признаны устаревшими (и категорически не рекомендуются к использованию), но до сих пор поддерживаются компиляторами «для совместимости»…
C++ гораздо удобнее (нам)
Если можно, было бы интересно узнать — по каким критериям удобнее?
Как я и говорил, мы отказались от сложных структур данных, и все реальные данные упакованы в std::vector<>. Но поверх этих векторов у нас есть zero cost abstractions, которые облегчают разработку. Насколько я знаю (внимание, я не специалист), всякие shared pointer возможны в фортране, но это нужно конкретно над ними работать. В цпп они доступны из коробки. Так что гибкость я бы не сбрасывал со счетов.
Мне очень интересно, что вы думаете об областях примения фортрана и цпп.
Если подробно, то мы уже 30 лет пилим очень узконишевую программу для научных исследований, причем основная часть сделана на фортране, а интерфейс — на плюсах. Я сам программирую только на фортране, за плюсы отвечает другой человек (и его код я понимаю ровно настолько, чтобы вносить туда совершенно косметические изменения вроде исправления опечаток в комментах ;-)).
Но что касается именно перемалывания чисел, то у фортрана это получается очень даже неплохо. Второй важный нюанс — весь фортрановский код, написанный нами лет 20 назад, до сих пор не только работает, но и при необходимости модифицируется с расширением функционала, и это не требует сверхусилий. Хотя наверно это зависит не только от языка… но для данного конкретного применения фортран вполне адекватен.
Но повторюсь ещё раз: очень мало людей знакомы с современным фортраном, можно даже сказать что это молодой язык, как бы парадоксально это ни звучало.
даже несмотря на то, что мы отказались от сложных динамических структур данных и всё упаковываем в большие массивы.если вам часто приходится итерироваться по многомерным массивам, лучше напишите нормальный многомерный массив, по которому можно итерироваться через range-based for, и используйте его:
for (auto& row : arr) {
for (auto& x : row) {
...
}
}
Отвечу как человек, которому приходится много кодить на Фортране. (При этом не могу сказать, что являюсь большим фанатом этого языка. Почему — дальше по тексту). У Фортрана есть много преимуществ для численных расчётов, плюс язык вышел из комы, в которой он находился в 90-х, и на данный момент существует более-менее постоянная комиссия, которая занимается развитием стандарта. Но у этого стандарта есть, как минимум, две проблемы: 1). обратная совместимость требует тащить с современный язык всякие архаизмы; 2). другая проблема, характерная и для многих других соверменных языков, — нет какой-то чёткой цели развития. В итоге, многие новшества добавляются ad hoc, приводят к усложнению, которое не всегда окупается потенциальными преимуществами. К примеру, начавшееся со стандарта 2003 движение в сторону ООП сильно притормозилось, а использование этих черт языка в нынешнем их виде зачастую приводит к сильному проседанию производительности. И никаких улучшений в виде zero-cost abstractions в следующем стандарте 202х не планируется.
Поэтому ситуация на данный момент такова, что если у вас уже есть значительная кодовая база на Фортране (не обязательно legacy, может быть стандарт 90 или 2003), то переходить на C++ или что-то другое смысла нет (тем более, что у C++ своих подобных проблем со стандартом вагон и маленькая тележка), т.к. на современном Фортране можно относительно комфортно писать высокопроизводительный код, если смриться с некоторыми корявостями. Если же у вас проект уже написан на C++, то я бы точно не советовал переходить на Фортран. На мой взгляд, будущее обоих этих классических языков довольно туманно, поэтому это будет смена шила на мыло. Если же начинать проект с нуля, то стоит уже смотреть в сторону чего-нибудь типа Julia. К сожалению, этот язык ещё не выбрался из стадии альфа.
(Речь идёт о численных расчётах, поэтому rust, go и далее по списку — не предлагать.)
На мой взгляд, будущее обоих этих классических языков довольно туманно
Это точно. Я пишу на C/C++ уже четверть века, и чем дальше, тем больше он меня пугает. Но внятной замены пока не вижу.
Чем D не внятен?
В нём сложно управлять памятью, например.
А в чём сложности?
Нет хороших компиляторов,
А чем существующие плохи?
отладчиков, профайлеров, IDE…
Можете использовать ту же Visual Studio.
А чем существующие плохи?
C++ компиляторы только-только догнали фортран по производительности кода. D ещё далёк от этого.
А вообще D — это мертворожденный язык (по моему скромному мнению). Он был создан для того, чтобы «починить» ошибки дизайна C++. Проблема в том, что никто не может сказать внятно, в чём именно ошибки. И тем паче как их чинить.
Сборщик мусора — страшная штука. D захотел «починить» проблемы с указателями в C++. Но только сборщик мусора — не решение проблем с указателями...
D ещё далёк от этого.
С чего вы взяли? Там довольно крутой компилятор, позволяющий использовать разные бэкенды, в том числе и популярные у плюсовиков GCC да LLVM.
Проблема в том, что никто не может сказать внятно, в чём именно ошибки.
В C++ много кривых мест, начиная с использования препроцессора и заканчивая UB во всех местах. В любой книжке по D об этом всём рассказывается.
Но только сборщик мусора — не решение проблем с указателями...
Конечно, поэтому вас его использовать никто не заставляет.
А что не так в rust с численными расчётами?
Последний раз, когда я смотрел, там, например, не было доступно "из коробки" ни комплексных чисел, ни многомерных динамических массивов с сечениями и всеми сопутствующими вещами. Новый язык без этих фич — сразу в топку. Rust не для численных расчётов создавался, в конце концов, и это влияет на вектор развития.
Я бы сказал, что отсутствие комплексных чисел вообще не аргумент. В C++ их тоже нет, однако считают на нём только в путь. И многомерных массивов нет. И сечений...
Мы же про удобство для конкретных целей говорим. Так-то на любом языке можно решать любые задачи. Хоть на vanilla C. Только библиотеки подавай.
Вообще говоря, никак не хотел обидеть растаманов, но язык же явно с другим фокусом создавался. Системное программирование, контроллеры, веб и т.д. Не вижу, какие такие преимущества он может принести для численного моделирования по сравнению с Fortran/C++, чтобы всё бросать и разбираться во всех этих тонкостях области действия, (не)изменяемости, unsafe и прочих специфических приблуд. При этом, для элементарных, с точки зрения численных расчётов, вещей уже нужно тащить даже не стандартные, а какие-то непонятно кем написанные библиотеки, API которых может внезапано поменяться или поддержка которых вообще может прекратиться.
На всякий случай поясню комментарий про API: время жизни очень большой части софта для численных расчётов должно измеряться десятилетиями. Поэтому на новомодный сырой язык мало кто перейдёт только потому, что это молодёжно. Очень, очень не зря современный фортран тащит на себе ярмо совместимости с предыдущими версиями стандарта.
Нас, например, очень напрягает то, что современный C++ убирает кучу старых фич, т.к. это ломает код.
Мы же про удобство для конкретных целей говорим. Так-то на любом языке можно решать любые задачи. Хоть на vanilla C. Только библиотеки подавай.
Не совсем. Некоторые вещи, отсутствующие в языке, невозможно восполнить библиотеками. В C, например, напрочь отсутствует обобщённые типы, так что при работе с матрицами разных типов там нужно либо обмазываться макросами, либо каким-то образом постоянно передавать информацию о типах и забыть о типобезопасности. А насчёт удобства — когда нужная библиотека добавляется добавлением одной строчки в конфиг проекта, разница между стандартной библиотекой и сторонней уже перестаёт быть столь существенной.
Не вижу, какие такие преимущества он может принести для численного моделирования по сравнению с Fortran/C++, чтобы всё бросать и разбираться во всех этих тонкостях области действия, (не)изменяемости, unsafe и прочих специфических приблуд.
При этом, для элементарных, с точки зрения численных расчётов, вещей уже нужно тащить даже не стандартные, а какие-то непонятно кем написанные библиотеки, API которых может внезапано поменяться или поддержка которых вообще может прекратиться.
Стандартную библиотеку тоже непонятно кто писал, но этим людям вы почему-то доверяете.
Не совсем. Некоторые вещи, отсутствующие в языке, невозможно восполнить библиотеками. В C, например, напрочь отсутствует обобщённые типы, так что при работе с матрицами разных типов там нужно либо обмазываться макросами, либо каким-то образом постоянно передавать информацию о типах и забыть и типобезопасности.
Шаблонные типы — это, безусловно, добавляет плюс в копилку rust'а, но они уже есть в C++, что сильно снижает мотивацию для переезда.
А насчёт удобства — когда нужная библиотека добавляется добавлением одной строчки в конфиг проекта, разница между стандартной библиотекой и сторонней уже перестаёт быть столь существенной.
Не так всё просто. По поводу библиотек — ниже.
Распараллеливание операций над матрицей с заменой по сравнению с однопоточным вариантом одной строки с гарантией отсутствия проблем с многопоточностью годится?
Это как раз то, что реализуется на уровне библиотек. В частности, подобные библиотеки уже очень давно есть и для C и для Фортрана (например, тот же, MKL). К тому же в контексте HPC чаще используется распараллеливание по распределённым процессам (используя MPI), а не по потокам. Хотя можно (но непросто) делать и то, и другое вместе.
Стандартную библиотеку тоже непонятно кто писал, но этим людям вы почему-то доверяете.
По поводу библиотек: самое главное отличие стандартной библиотеки заключается в том, что её API задан неким стандартом. Это значит, что он (интерфейс) не будет меняться по велению левой пятки его создателя. А если и будет меняться, то это будет делаться в рамках последовательного процесса (standard -> obsolete -> deleted), растянутого на годы, а иногда и десятилетия. Подобная консервативность — это именно то, что ставит древние языки на первое место при выборе инструментов для долгосрочного проекта.
Сразу замечу: всё это совершенно не означает, что нужно теперь всю жизнь сидеть на Fortran/C++, игнорируя прогресс в языкостроении. В некотором смысле, я уже давно жду "ту молодую шпану", которая сметёт этих старичков. Но если посмотреть на то, что находится в разработке на данный момент, то для численных расчётов выбор, скорее, падёт на что-нибудь типа julia, чем на rust. Достаточно взглянуть на базовую библиотеку (Base) julia, с огромным количеством матфункций (которые, кстати, работают и с комплексными числами, что важно), с кучей операций над массивами (они, похоже, собрали всё, что есть в fortran'e и matlab'e на этот счёт), чтобы понять, что этот язык делали с прицелом на расчёты. Я не уверен, что даже в сторонних библиотеках rust'а найдётся большая часть того функционала, который уже сейчас есть у julia.
Распараллеливание операций над матрицей с заменой по сравнению с однопоточным вариантом одной строки с гарантией отсутствия проблем с многопоточностью годится?
Если я правильно посмотрел, там по ссылке по сути заменяется обычный "map" для обхода массива на его параллельную реализацию "threaded map". Это во многих языках есть из коробки или почти из коробки — если что-то не так понял, то поясните пожалуйста.
Вот каким должен быть идеальный цикл — http://cl-cookbook.sourceforge.net/loop.html :)
А зачем это делать отдельным макросом, если это прекрасно пишется на итераторах?
Ну я ради стеба привёл эту ссылку, просто в Common Lisp loop это целый язык, сложнее самого Lisp :)
(loop
for item in list
for i from 1 to 10
do (something))
(loop for i from 0 downto -10 collect i)
(loop for v being the hash-values in h using (hash-key k) ...)
(loop repeat 5
for y = 1 then (+ x y)
for x = 0 then y
collect y)
(loop for i in *random*
counting (evenp i) into evens
counting (oddp i) into odds
summing i into total
maximizing i into max
minimizing i into min
finally (return (list min max total evens odds)))
Можт, писали:
У нас в java так:
collection.forEach(item -> {
[..]
});
Кратко и понятно
'for(var item: collection) {...}' — более краткий вариант.
Не согласны — предлагайте мнение / варианты! Минусовать не стесняйтесь, но мне интересны мнения!
Не согласны — предлагайте мнение / варианты!
Казалось бы, вместо первого цикла в начале поста прямо напрашивается вынести соответствующие вещи в отдельные функции/итераторы, чтобы записывалось как-то так:
for (auto iter: range(1, n_iters)) { // some iterative computation
for (auto cell: mesh.cells()) // loop through all tetrahedra cells
for (auto [v1, v2]: subsets(cell.vertices(), 2)) // for every pair of vertices
for (auto d: range(1, 3)) { // for each of 3 dimensions
nlRowScaling(weight);
nlBegin(NL_ROW);
nlCoefficient(v1, d, 1);
nlCoefficient(v2, d, -1);
nlEnd(NL_ROW);
}
[...]
}
Выглядит чище и понятнее, сложнее с индексами ошибиться.
Да, именно к этому сейчас и пришли. Вы только типы забыли перед объявлением переменных.
В посте не написано, что к этому пришли — поэтому и предложил :) Про типы — да, действительно забыл (простительно т.к. не пишу на c++), добавил auto везде.
По факту с индексами вообще редко приходится напрямую оперировать — в том числе в численных научных вычислениях, которые иногда пишу.
Я auto стараюсь не использовать в циклах (ну, помимо structural binding в enumerate/zip), т.к. бывает, что переменная целочисленная, а бывает, что и ссылку возвращаю. Ну и вообще auto на один символ длиннее int :)
for i, v := range arr {}
Он работает и как enumerate() и как range() в python, границы можно указать через создание слайса arr[low, high, max] либо переписав цикл в классическом виде, ненужные значения можно просто закомментировать символом «_».
Да, все таки такие большие трех-, четырех- и больше мерные проходы по матрицам, массивам и т. д. стоит выносить в отдельную, с говорящим названием, функцию. Ибо я, например, сидящий на JS, как бы не крутился, все равно получаеться громоздко и неудобно (хоть через foreach() хоть через тот же for)
У вас конструкция из 5 вложенных циклов. Это операция такой сложности для понимания и отладки, что макросами и синтаксическим сахаром вы можете ее сделать только менее понятной для стороннего человека. У вас отличные комментарии в коде, сам он весьма читабельный. Если в первую секунду не ясно, что и как итенируется, то потом видны комментарии и все становится на свои места. Смешайте это на половину с for-range или for each и читабельность упадёт. Буквально мне не нравятся только константы 4 и пр. в цикле.
На ваш взгляд, каким должен быть совершенный цикл в 2020м году?Примерно таким, как у вас в статье, только в C++20 (23) можно использовать стандартные функции вместо самописных. range -> iota (https://gcc.godbolt.org/z/d3bqYK). enumerate и zip должны появиться в C++23, так это выглядит в range-v3 (https://gcc.godbolt.org/z/9EfMj5):
#include <range/v3/view/enumerate.hpp>
#include <range/v3/view/zip.hpp>
#include <vector>
void test(std::vector<int> xs, std::vector<float> ys) {
for (auto [x, i] : ranges::view::enumerate(xs)) {
// ...
}
for (auto [x, y] : ranges::view::zip(xs, ys)) {
// ...
}
}
Вместо вложенных циклов можно (но не факт что нужно) использовать cartesian_product (https://gcc.godbolt.org/z/vaasqq):void test(std::vector<int> xs, std::vector<float> ys) {
for (auto [x, y] : ranges::view::cartesian_product(xs, ys)) {
// ...
}
}
Кстати, в c++20 появился std::ranges, что ещё больше упрощает написание такой функции, но я пока не готов переходить на этот стандарт.
Ну а пока есть FunctionalPlus (C++14), range-v3 (C++14/17/20) и NanoRange (C++17).
test "for basics" {
const items = [_]i32 { 4, 5, 3, 4, 0 };
var sum: i32 = 0;
// For loops iterate over slices and arrays.
for (items) |value| {
if (value == 0) {
continue;
}
sum += value;
}
assert(sum == 16);
for (items[0..1]) |value| {
sum += value;
}
assert(sum == 20);
var sum2: i32 = 0;
for (items) |value, i| {
assert(@TypeOf(i) == usize);
sum2 += @intCast(i32, i);
}
assert(sum2 == 10);
}
test "for reference" {
var items = [_]i32 { 3, 4, 2 };
for (items) |*value| {
value.* += 1;
}
assert(items[0] == 4);
assert(items[1] == 5);
assert(items[2] == 3);
}
test "for else" {
var items = [_]?i32 { 3, 4, null, 5 };
var sum: i32 = 0;
const result = for (items) |value| {
if (value != null) {
sum += value.?;
}
} else blk: {
assert(sum == 12);
break :blk sum;
};
assert(result == 12);
}
Замечу, что в Zig нет итераторов, но можно определить внутреннюю структуру (обычно Iterator) и c функциями next, prev и т.д. и использовать while:
var it = somevar.iterator();
while (it.next()) |value| {
...
}
В предстоящем релизе значительно улучшены compiletime-возможности (помимо прочего), так что у Zig большое будущее. :)
P.S. Используется подсветка кода С++.
range
. Только, я бы вынес всё, что Вы писали внури тела функции в отдельный класс и кроме варианта range (end)
добавил бы range (begin, end)
и, быть может, range (begin, end, step)
. Но куда более удобным вариантом будет сделать класс шаблонным. Тогда можно будет писать, например, for (auto i : range (it1, it2))
.Именно поэтому я написал эту обёртку с range; но справедливости ради любой нормальный IDE (MS Visual Studio, например) при наведении мышки на FOR(i, n) покажет определение дефайна.
Конкретно наш код будет читаться только членами нашей команды, поэтому локальные загогулины допустимы, хотя и не очень желательны.
Я не против макросов в целом, я против их использования в тех местах, где синтаксис языка и так неплохо справляется.
Выше уже упоминали D, а если вспомнить, что все эти ranges в C++ попали оттуда (именно в виде шаблонных конструкций этапа компиляции), то и for надо смотреть там же.
foreach (i; 0..100) {}
В C++ это переносится примерно так же, как у вас в статье
for (auto i : iota(100)) {}
Я считаю, что синтаксический сахар в виде range-based for вместе с хорошей библиотекой ranges (ranges-v3, которая фактически в стандарте C++20), и есть идеальный for.
Ну и стоит упомянуть, что функциональщина в виде map, filter или cartesianProduct часто лучше читается. Можете посмотреть на ndslice и увидеть, как хорошо решается задача многомерного прохода на тех же абстракциях.
В D, кстати, тоже есть iota
:
foreach (i; 100.iota) {}
Да, но 0..100 читается лучше. iota хороша для функциональных цепочек с map, filter, chunk и тд, и ещё для шага отличного от 1, он там третьим аргументом передаётся.
Имхо, это недоработка в языке, что 0..100 это только для foreach. Было бы лучше, если можно было бы в любом месте использовать для обозначения диапазонов. Вместо этого… переиспользовали для срезов и значение там совсем другое.
Самый лучший foreach что я видел — у Kotlin
list.forEach {
println(it)
}
На scala аналогом будет
list foreach println
А если нам нужно использовать каждый элемент несколько раз?
list.forEach {
action1(it)
action2(it)
...
}
Пожалуйста:
list foreach { it=>
action1(it)
action2(it)
}
Вложенные циклы так даже удобнее:
list1 foreach { a=>
list2 foreach { b=>
use(a, b)
}
}
Но, конечно, второй случай можно записать более традиционно:
for(a<-list1; b<-list2) {...}
Kotlin задумывался как более простая scala. it-синтаксис вообще из groovy.
Так то ваш пример в kotlin не сильно длиннее будет
list.forEach(::println)
Чем этот forEach удобнее котлиновского же цикла for, да и цикла for в других языках?
list.forEach {
println(it)
}
vs
for (it <- list) {
println(it)
}
for i, j, k in itertools.product(range(4), range(n), range(3)):
# do something
Чёрт его знает, как такую штуку реализовать на C/C++, но раз тут мозговой штурм…
Ещё если в проекте много повторяющихся геометрических итераций вида тетраэдры->вершины->грани, то можно заранее создать стандартные списки индексов, по которым потом итерироваться — такой кэшированный результат itertools.product, что снимет проблемы с производительностью.
Когда-то давно этим баловался http://rsdn.org/forum/src/1560894.1
Практика в те годы показала что добиться аналогичной генерации не так легко.
Кроме того такой код менее понятен другим.
Очень редко пишу вложенные циклы. Только если вложенный итератор от внешнего зависит. В остальных случаях, естественно, for I, j, k in itertools.product().
Совершенный цикл for