Comments 45
А в чем отличие от C/C++ Extensions for Array Notations? Это расширение языка было взято из Intel Cilk и поддерживалось когда-то классическим интеловским компилятором.
result[>i] = e[i][>j][>k]
a[>j]
b[>k];
Лёгким движением руки... 6 слагаемых превращаются в 27...
Матрица e, вообще говоря, состоит из констант. Здесь надежда на то, что компилятор сообразит, что будет, если выражение умножить на 0, 1 или -1.
Интересно, но я бы разделил свёртку от итерирования, чтобы можно было делать и поэлементые операции, да и свёртки бывает нужны разные, а не только суммирование.
И как предлагается перегружать этот оператор?
я бы разделил свёртку от итерирования
У меня была мысль для чистого итерирования использовать ещё два новых оператора:
|> проход от начала к концу
<| проход от конца к началу
но это была смутная идея, и я её не прорабатывал. Тем более, что при той записи, которую я предложил в статье, вполне можно разделить итерирование и свёртку для стандартных случаев: если декларация индекса не повторяется в одночлене - это итерирование, а если мы декларировали его повторно в том же одночлене - свёртка.
И как предлагается перегружать этот оператор?
Сам по себе оператор не перегружается. В принципе, обычным образом перегружается обращение к элементу массива по его индексу (operator []), и перегружаются операции сложения и умножения. Теоретически, можно было бы добавить какую-то специальную операцию умножения, которая на входе принимает два множества объектов, и что-то с ними делает, но это слишком всё усложнит.
У меня была основная идея в том, чтобы простым образом задавать умножение нестандартных матриц. То есть, теперь можно, как в последнем примере статьи, рассматривать массив объектов, у которых вектором-строкой является одно из полей, как полноценную матрицу.
Вопрос-то не в этом был.
У вас в записи скалярного произведения
float result = a[>] * b[>];
умножение явно прописано, а сложение подразумевается.
Это плохо.
Почему там именно сложение, а не какая-то другая операция, задаваемая пользователем?
Только ради того, что сейчас у вас именно такая задача?
Почему там именно сложение
Я стремился к лаконичности синтаксиса, а это - наиболее стандартный случай.
а не какая-то другая операция, задаваемая пользователем?
Ну, можно, например, так усложнить запись:
float result = a[>] *:+ b[>];
Сначала мы поэлементно выполняем умножение, затем то, что получилось - складываем между собой.
Тогда не понятно как использовать произвольные функции вместо операторов. Ну а перегрузки нужны, чтобы иметь тот же лаконичный синтаксис и со своими типами данных.
Тогда не понятно как использовать произвольные функции вместо операторов
Было бы проще, если бы вы привели пример, где это нужно.
Сформулируем задачу: сначала применяем поэлементно к двум векторам какую-то бинарную функцию, затем все элементы вектора результата обрабатываем другой функцией, аналогичной сложению. Для этого можно придумать такую запись:
class Obj {/*...*/};
Obj mul(Obj x, Obj y) {/*...*/}
Obj add(Obj x, Obj y) {/*...*/}
Obj a[10], b[10];
// ...
Obj result = a[>] (mul:add) b[>];
Если нужно найти максимальное значение вектора, то это можно было бы так записать:
float a[10];
// ...
float result = a[>] (: [](float x, float y) {return x > y? x: y;});
Здесь внутри (: ...) мы задаём функцию от двух аргументов, которая сначала применяется к двум первым элементам вектора, затем её результат будет использоваться как левый аргумент этой же функции на следующих итерациях, а правый будет очередным элементом вектора. И так до конца вектора.
Если вектор имеет размер равный 1, то возвращается его первый элемент. Для учёта вектора нулевого размера можно дополнительно указать результат для этого случая после ключевого слова else.
Например, сумма всех элементов вектора:
float a[10];
// ...
float result = a[>] (: [](float x, float y) {return x + y;} else 0);
Уже лучге, можно было бы записать скалярное произведение
float result = (a[>i] * b[>i]) (: [](float x, float y) {return x + y;} );
Разделение операций по конвейеру (сначала делаем одно, потом результат передаём другому) очень похоже на ranges в новом стандарте.
Жаль конечно, что синтаксис лямбд в C++ не смогли сделать лаконичным.
Во многих языках уже ввели стрелочную нотацию (x,y) => x+y
с авто-выводом типов параметров и результата. В таком виде ranges были бы очень хорошо читаемыми.
В D можно вообще так писать: list.map!q{ a + b }
А почему конкретно a и b? Там такое соглашение, что параметры безымянной функции - это a, b, c, ... ? А если переменная b объявлена во внешней области видимости, есть способ к ней обратиться?
В D можно вообще так писать: list.map!q{ a + b }
В моих обозначениях можно даже короче записать:
auto sum = list[>] (:+);
Вместо бинарной функции мы передаём бинарный оператор.
Почему там именно сложение, а не какая-то другая операция, задаваемая пользователем?
Был такой же вопрос, но с другой стороны фактически это Эйнштейновская тензорная нотация, он так же обращался с немыми индексами, и да, практически всегда нужно сложение.
https://ru.wikipedia.org/wiki/Соглашение_Эйнштейна
Потом кто это месиво читать будет?
Создайте класс для подобных операций и все у вас будет хорошо.
Даю голову на отсечение, что и сам автор лет через 30, глянув на свой же код будет долго рвать волосы на голове и других местах в попытках понять, что же все-таки в этом коде происходит. Это сейчас: да... по горячим следам, автору все кажется очевидным и само-собой разумеющимся (по себе сужу, да - я уже стар )
Вопрос к автору: чем вам не угодила перегрузка операторов? Вы можете для класса вектор перегрузить операцию - умножение на матрицу и писать примерно так:
Vector a = new Vector(1,2,3);
Matrix m = new Matrix(1, 0, 1
2, 3, 2
1, -1, 4);
Vector c = a*m;
// умножение матриц
Matrix scaling = new Matrix(...);
Matrix translate = new Matrix(...);
Matrix rotate = new Matrix(...);
// деклаем матрицу, котора сразу и масштабирует и переносит и вращает
Matrix transform = scaling * translate * rotate;
// считаем определитель матрицы
double det = transform;
Так будет более понятно и не только вам, но и другим читателям кода. А все потому, что многие уже знают (со школы или университета), как работает умножение вектора на матрицу, и какой в итоге должен быть результат.
Нравится функциональный стиль? - пишите на соответствующем языке. На Haskel, например, можно так загнаться, что вы уже и через неделю не сможете разобраться в своем коде.
Даю голову на отсечение, что и сам автор лет через 30, глянув на свой же код будет долго рвать волосы на голове и других местах в попытках понять, что же все-таки в этом коде происходит.
Запись полностью аналогична записи в тензорных обозначениях, где по повторяющимся индексам подразумевается суммирование. В физике такую запись используют как раз потому, что она удобнее и понятнее, чем безиндексная.
чем вам не угодила перегрузка операторов?
На Хабре уже была хорошая статья на тему того, почему умножение матриц без индексов - слишком неудобная операция: https://habr.com/ru/articles/910834/
Мне интересно, как вы будете умножать матрицы не строка на столбец, а строка на строку? А если строка в поле объекта, как у меня в последнем примере? Каждый раз будете оператор переопределять? Больше кода = больше ошибок.
Нравится функциональный стиль?
В функциональном стиле там только переворачивание вектора. Я добавил это просто как интересную возможность использования прохода в обратном направлении. Это вполне можно выбросить - на умножение это не повлияет.
Запись полностью аналогична записи в тензорных обозначениях, где по повторяющимся индексам подразумевается суммирование.
Только там она не называется скалярным произведением, это свёртка. Зачем Вы тогда её так называете?
Вообще для работы с матрицами есть специализированное ПО, тот же Матлаб.
Только там она не называется скалярным произведением, это свёртка. Зачем Вы тогда её так называете?
Тут любое определение будет неточным.
Свёртка - это когда у одного и того же тензора два индекса одинаковы, и по ним производится суммирование. Скалярное произведение формально состоит из двух операций: сначала мы находим объект, состоящий из произведений каждого компонента одного вектора на каждый компонент второго, после чего проводим свёртку по обоим индексам этого объекта.
Вообще для работы с матрицами есть специализированное ПО, тот же Матлаб.
Мне просто пришла интересная идея в голову, и я решил ей поделиться. В язык C++ это, разумеется, никто вносить не будет - но на Хабре хватает людей, которые пишут свои языки программирования, и им это может быть интересно.
Скалярное произведение — это свёртка с метрическим тензором. Общая формула c=Gij u^i v^j. Только в ортонормированном базисе скалярное произведение выражается как сумма попарных произведений координат. Если базис другой, будет положительно определённая билинейная форма.
object IsHCons1 {
type Aux[L[_], FH[_[_]], FT[_[_]], H0[_], T0[_] <: HList] = IsHCons1[L, FH, FT] { type H[t] = H0[t] ; type T[t] = T0[t] }
def apply[L[_], FH[_[_]], FT[_[_]]](implicit tc: IsHCons1[L, FH, FT]): Aux[L, FH, FT, tc.H, tc.T] = tc
implicit def mkIsHCons1[L[_], FH[_[_]], FT[_[_]]]: IsHCons1[L, FH, FT] = macro IsHCons1Macros.mkIsHCons1Impl[L, FH, FT]
}
Вот такой, например, понятный код. источник https://habr.com/ru/articles/259841/
Синтаксис Matlab содержит два умножения, типа а.*б и типа а*б, поэлементное и векторное. Может, проще сделать оператор точка-звездочка?
Кажется, что предлагаемый синтаксис подходит только для одной очень узкой задачи. Шаг в сторону - и, по всей видимости, нужно всё переписывать на обычные циклы. Например, если я хочу шаг, отличный от 1? А если я хочу на каждом шаге цикла еще залогировать что-то? Ну и так далее
В каждом языке есть конструкции, о которых со временем понимают, что можно было организовать для них какой-то синтаксический сахар. А ведь когда-то давно в девяностых во всех разновидностях Basic-а или Pascal/Delphi очень не хватало «i++» инкремента и приходилось лепить конструкции вида «i=i+1», что прям сильно подбешивало. А ещё копировать массивы в цикле, тогда как весь цивилизованный мир использовал указатели/передачу по ссылке. Нет, ну способы были, конечно, но это небыло нативно, лаконично, удобно... Хорошо хоть строку - в виде массива символов - не надо было присваивать вручную по-одному... Если какие-то примитивы и инструкции уже оптимизированы и отработаны на уровне языка, то почему бы и нет? Оптимизация рутинных действий - итерировать, передать по цепочке, обработать базовые структуры - я за! Пока что такие операции вываливаются в изобретение велосипедов и эти самые велосипеды будут защищать, вместо того, чтоб от них избавляться.
В Delphi всегда был inc(I) и массивы всегда передавались по ссылке. А строки в Delphi всегда были куда проще в использовании, чем в С/Срр.
Если точнее, то inc появился ещё в турбо-паскале. У строк интрерфейс был удобный, но с нюансом про короткую длину строки.
Ещё точнее, это Вирт придумал.
Он загонялся по математической строгости, пытался различать ординалы и кардиналы. У него функции succ/prev и соответствующие им процедуры inc/dec, это не просто прибавить/убавить единицу, а выбрать предыдущий/следующий элемент в упорядоченном множестве, не обязательно числовом.
В те времена длины строки хватало, а сейчас длина до 2гб. И в редакторе кода можно в переменную вручную затолкать пару тысяч символов.
В Delphi ... массивы всегда передавались по ссылке
Насколько я знаю, в примере ниже передаются по значению (создаётся копия массива)
procedure test(str: array [1..40] of char);
как и тут
procedure test(str: string[40]);
По-моему, в текущем виде это всё очень притянуто за уши и слишком "местечково": если слева от "=" - то итерируем, если справа от "=" то что-то итерируем-складываем. Т.е. в одном случае одномерное остаётся одномерным, а в другом случае одномерное становится скаляром.
Возможно, выходом было бы отделить итерирование от всего остального. Например, ваш пример:
C[>i][>j] = A[i][>k] * B[>k][j];
Превратить во что-то:
C[>i][>j] = SUMM(A[i][>k] * B[k][j]);
Какие отличия:
явное задание количества "итерирований": есть по i, j, k - по одной штуке. У
B[k][j] своей итерации нет
;результат
A[i][>k] * B[k][j] - это одномерный массив (так как всего одна итерация по k) из результатов умножений;
функция суммирования SUMM задаётся явно, принимает массив чего-то, возвращает сумму. Технически сюда можно и пользовательские функции затолкать.
Как результат, получаем в целом компактную запись с отработкой всяких случаев, когда элементами массивов являются разные штуки (условно: структуры, которые можно умножать и складывать).
Также, отдельно лучше/придётся продумать, когда индексы не-числовые: например, когда значениями индекса являются строки (или другие абстрактные ключи).
Вся проблема в том что вы хотите использовать выражения для вычислений. А ведь можно просто разбить выражение на функции и обернуть их в функцию и её уже использовать по месту. Примерно так:
new: float result = a[>i] * b[>i];
old: float result = smul(a,b);
new: a[>i] = i * i;
old: for(auto i:index(a)) a[i]=i*i;
new: a[>] = 0;
old: load(a,0);
new: result[>i] = a[i] * b[i];
old: mul(r,a,b);
new: a[>i] = a[<i];
old: reverse(a);
и не понадобиться изобретать инопланетный синтаксис к уже существующему.
Я добавил в конец статьи ссылку на статью о проблемах безындексной записи умножения тензоров: Я не люблю NumPy
Вам надо было упомянуть Эйнштейна, хотя бы для рекламы :). Ведь фактически это Эйнштейновская тензорная нотация, он так же обращался с немыми индексами, и так же подразумевал сложение.
https://ru.wikipedia.org/wiki/Соглашение_Эйнштейна
Возможное расширение языка C++ операцией скалярного произведения