Здравствуйте, Qbit86, Вы писали:
Q>Алгоритмы, использующие обобщённое сравнение всем известны: сортировки, деревья, хэш-таблицы. Использование компаратора ни у кого удивления не вызывает.
Q>Алгоритмы, использующие обобщённое сложение никому кроме меня не известны: например, алгоритм Дейкстры на графах. Поэтому использование моноида вызывает бурю эмоций.
Не, никакой бури не вызывает. То же самое матричное умножение вполне можно использовать для вычисления транзитивного замыкания на матрице смежности, если * заменить на &&, а + — на ||.
Или для поиска кратчайших путей — тот же Дийкстра, только для всех пар вершин сразу.
Q>Для обобщённого сравнения ты реализуешь IComparer<T> или IEqualityComparer<T>. Аналогично, для обобщённого сложения (обобщённой бинарной операции) ты реализуешь IMonoid<T>. Или, если ты больше не инженер, а уже менеджер, то IGenericAggregationControllerManager<T>, так трудовому народу понятнее.
Это всё как раз понятно.
S>>1. Рассказ про то, что у инта сразу два моноида, был убедителен, но непонятно, как именно это можно применить. Ну, кроме вырожденного случая "давайте проагрегируем массив интов с использованием 1 и * в качестве нашего моноида".
Q>Давай не про инты, а про float'ы, ок? Аддитивный моноид понятно, например, складывать веса дуг вдоль пути на графе. Мультипликативный моноид, например, когда у тебя цепочка процентных ставок: начинаем с 1.0, заплатили налог 13% (домножить на 0.87), получили interest 5% (домножить на 1.05) и так далее.
А какая разница? Хоть decimal. Ничто не мешает нам строить моноид хоть над string — в нём же даже коммутативность не нужна.
Я говорю о том, что предлагаемый вариант
ролей не шибко помогает.
Q>Точно так же, как у тебя могут быть разные компараторы для одного типа (сравнение строк с учётом регистра, без учёта культуры, etc.), так и моноиды могут быть разные для одного типа.
Q>Взя затея вокруг шейпов — это добавить чуть синтаксического сахара вокруг всех этих IEqualityComparer<T>'ов и IMonoid<T>'ов, чтоб передавать не инстансы (они ж все одинаковые stateless), а сами типы.
Рулится прямо сейчас:
interface IMonoid<T>
{
T Zero {get;}
T Add(T t1, T t2)
}
public class Add<M>
where M: IMonoid<T>, new
{
public static T All<T>(T[] ts)
{
var m = new M();
var T result = m.Zero;
foreach(vat t in ts)
result = m.Add(result, t);
}
}
class IntAddMonoid: IMonoid<int>
{
public int Zero{get=>0;}
public int Add(int t1, int t2)=>t1+t2;
}
int sixtyThree = Add<IntAddMonoid>.All(new[]{1, 2, 4, 8, 16, 32});
С учётом того, что статические интерфейсы ты ничем не параметризуешь, дефолтного конструктора вполне достаточно.
Вся разница — код работает прямо сейчас, не дожидаясь C# 12.
Что было бы ценно — возможность биндить моноиды, кольца и группы к типам в обобщённом коде без костылей.
Чего мы хотим? Чтобы AddAll просто работал просто из коробки, с любыми Numeric-типами, включая Complex и прочую экзотику. Вот extension для этого — отличная идея.
А с ролями — не вижу смысла. Чтобы можно было использовать AddAll прямо в таком виде для того, чтобы вычислять минимум ряда, передав Min(a, b) в качестве +?
Простите, не вижу ценности по сравнению с простым кодом выше. Ну, ок — если добавить статические интерфейсы, то можно будет убрать строчку var m = new M(), и просто обращаться к M.Zero и M.Add.
С моей точки зрения, для
обобщённой операции сложения использовать оператор "+" — баловство.
Операторы удобны там, где есть более-менее сложные формулы. Ну, там
(a[i-1]+2*a[i]+a[i+1])/4. Вот такое переписывать в стиле "вызов метода" — плохо, теряется читаемость.
И как раз такие формулы имеют смысл для обобщённых numeric типов. И как раз в них сложения и умножения — это сложения и умножения, а не "обобщённые сложения".