Pull to refresh

Comments 33

Идея довольно простая, на самом деле. Но с моноидом внутри, действительно, получается красиво.

Есть два подхода к программированию. Первый — сделать программу настолько простой, чтобы в ней очевидно не было ошибок. А второй — сделать её настолько сложной, чтобы в ней не было очевидных ошибок.

Tony Hoare

Можно посчитать сумму параллельно, а потом, получив её на финальном узле, за O(1) -- max.

Пример последовательности: 1 -2 3. Если считать правильно то получится: relu(1-2)+3=3; а если наивно: 2.

Легко понять что порядок важен: если переставить 3 в начало то переносов при сложении не возникнет, и результат совпадет, в то время как подход "сложить все" - теряет информацию о порядке.

Ясно, max применяется на каждом шаге, а не ко всей сумме.

Забавно :) Но не понятна область применения.

Пример последовательности: 1 -2 3

Если это как-то относится к остаткам на складе или состоянию счета, то на этапе -2 нужно генерировать exception. Банкомат, например, либо выдает заказанную сумму, либо не выдает ничего.

Очень правильный вопрос. Подобрать подходящий инструмент под задачу и наоборот — полдела к успеху в любом начинании. Могу предложить варианты для grosum:

  • Кладовщик ограничен максимальной пропускной способностью — на входе потенциальный расход, а не фактический, который меньше.

  • Банкомат выдаёт максимум, сколько может.

  • Глязные данные, которые нужно почистить online или offline.

  • Запоздалые транзакции оформлены задним числом. Нужно быстро инкрементально за O(ln n) пересчитать хвост журнала.

Запоздалые транзакции оформлены задним числом.


Это воще всё ломает. Если такое допустимо (транзакция уже оформлена) — её нельзя не проводить. Без овердрафта никак.
Сложно прогнуть реальный мир под стройные математические абстракции.

Но можно использовать конструкцию из статьи для валидации журнала, чтобы определить всё ли в порядке, или явно забыта ещё какая-нибудь транзакция.

О, кстати про исключения. Я только что понял, что случай "суммы с исключениями", во-первых, точно так же не является ассоциативным, а во-вторых вычисляется тем же самым алгоритмом, только с проверкой fall на последнем шаге:


static long grosumEx(LongStream changes) {
    Gro x = changes.collect(Gro::new, Example::accumulator, Example::combiner);
    if (x.fall > 0)
        throw …;
    return x.grow;
}

Но это только для случая, когда любое исключение ломает процесс. Если рассматривать модель банкомата, который при исключении продолжает работу с неизменным остатком (a+b<0? a: a+b) — то пара ⊕a ⊖b из моего комментария ниже перестанет редуцироваться до ⊖(b — a) и вся конструкция рассыпается.

В статье показана магия, но явно не хватает объяснений. И группа Гротендика тут ничего не объясняет, а лишь усложняет, потому что, к примеру, сложение в группе Гротендика отличается от операции combine.


Поэтому попробую раскрыть магию подробнее. Вот у нас есть операция ⊕, которая "неудобна" из-за отсутствия ассоциативности. Сопоставим теперь каждому числу a функцию fa(x) = x ⊕ a, эта функция выражает наше "намерение" прибавить a. Обозначим такую функцию (⊕a).


Теперь рассмотрим пару чисел a и b, и функцию-"намерение" для этой пары: ga, b(x) = (x ⊕ a) ⊕ b. Эта функция является композицией функций ⊕a и ⊕b: ga, b = (⊕b) ∘ (⊕a). Кстати, обозначим для простоты в дальнейшем такую функцию как ⊕a ⊕b. Теперь вспомним что композиция функций всегда ассоциативна, и мы получаем, что любую операцию можно "сделать" ассоциативной если перейти от исходного множества и операции к множеству функций-"намерений" над первым и композиции функций.


Кстати, по-научному "намерения" называются эндоморфизмами, а построенный нами моноид так и называется — моноид эндоморфизмов.


К сожалению, операция композиции функций ничего не вычисляет, а лишь откладывает вычисление, поэтому "в лоб" всё перечисленное выше не применить. Нужно найти какой-то способ упрощения сложных композиций, чтобы закодировать эндоморфизмы пригодным для вычислений способом.




Теперь переходим к самой "магии". Для начала, разделим все эндоморфизмы по знаку: здесь и далее,


⊕a означает функцию fa(x) = x ⊕ a, где a ≥ 0
⊖a означает функцию fa(x) = x ⊕ -a, где a ≥ 0


Заметим, что намерения одного знака друг с другом складываются:


⊕a ⊕b = ⊕(a+b)
⊖a ⊖b = ⊖(a+b)


Также заметим, что операции ⊕ и ⊖ могут компенсировать друг друга, но только если идут именно в этом порядке:


⊕a ⊖b = ⊕(a — b), если a ≥ b
⊕a ⊖b = ⊖(b — a), если a ≤ b


(случай a=b тут разобран два раза, но это не ошибка, потому что ⊕0 и ⊖0 — одна и та же операция, и нет разницы как считать)


А вот последовательность операций ⊖a ⊕b никак не упрощается, поэтому в общем случае любая последовательность операций упростится до этой формы. Ну так давайте именно такую форму и выберем в качестве конструктивного представления "намерения"!


И так, любой интересующий нас эндоморфизм мы будем представлять в виде пары чисел (a, b): f(x) = (x ⊕ -a) ⊕ b, где a и b ≥ 0.


Теперь можно наконец-то выразить операцию композиции намерений:


(a, b) ⊕ (c, d) = ⊖a ⊕b ⊖c ⊕d = ⊖a ⊕(b-c) ⊕d = ⊖a ⊕(b-c+d) = (a, b-c+d), если b ≥ c
(a, b) ⊕ (c, d) = ⊖a ⊕b ⊖c ⊕d = ⊖a ⊖(c-b) ⊕d = ⊖(a-b+c) ⊕d = (a-b+c, d), если b ≤ c


Проверяйте, эта формула соответствует функции combiner из поста. Функцию accumulator можно получить, если подставить в эту формулу формулу перехода от числе к намерениям. Ну а финальное получение поля grow в функции grosum — это ни что иное как подстановка x=0 в формулу эндоморфизма:


(a, b)(0) = (0 ⊕ -a) ⊕ b = 0 ⊕ b = b.

👍 в копилку объяснений

А вот применять неотрицательную сумму к складу я бы поостерёгся: отрицательный остаток на складе — явно исключительная ситуация, и молча исправлять такие ситуации в ноль — не совсем то, что ожидается от надёжной системы.


Опять-таки, если из пустого склада увезли 16 единиц продукта, а потом эту операцию сторнировали (что было отражено в журнале как +16 единиц продукта) — в итоге остаток должен быть 0, а не 16.

Зависит от целей, а не от того, что это склад. У нас, например, оптимизационная NP задача составления расписаний сотрудников, в том числе на складе. А быстрый инкрементальный пересчёт позволил считать на ноутбуке, а не на кластере.

Абстракция отличается от реальности и выбирать нужно подходящую под действия. grosum ничем не лучше и не хуже sum — ещё один кирпичик конструктора приложений.

Кстати, в поле fall накопится сумма, которую нужно сторнировать, чтобы баланс сошёлся. Тоже может быть кому-нибудь полезно.

Э-э-э, ну уж нет. Коли история содержит недостоверные данные — сумма, которую нужно сторнировать, может быть найдена только по результатам инвентаризации.

Большое спасибо за статью. Порекомендуйте пожалуйста литературу по функциональному программированию. У меня каша в голове от всех возможностей которые даёт фп. Хотелось бы привести это всё в порядок, но достойной книги где бы это всё хорошо разжёвывалось я пока не нашел. Заранее большое спасибо за любые ссылки или любую информацию.

К сожалению "имена" не добавляют ясности и понятности.

Я раз пять прочитал Ваше "определение" Вариантности......ни чего не понял. Думаю что и после 50 прочтения тоже ничего не пойму..... Мне кажется дело не во мне, а в "определении" ))))) Возможно я ошибаюсь. (

Хотя Вариантность достаточно простая и понятная штука. Если конечно её просто и понятно объяснять.

Может есть смысл улучшить Ваше "определение" Вариантности?

Возможно пример того, как Вариантность можно использовать в реальном проекте, сделает Ваше "определение" Вариантности более понятным.

К сожалению, только читать бесполезно, хоть 5, хоть 1000 раз. В том посте ошибка — "определение" надо переназывать "понятиями" — сам только недавно научился различать. Прочитав множество имён снега, вы не научитесь ими пользоваться. Попробуйте написать для детей простой туториал езды на велосипеде. Мы можем понять друг друга только в деятельности.

Человеку вот понравилось описание функторов и монад, но думаю только потому, что он уже достаточно попрактиковался, наработал понятие и прочувствовал несколько примеров монад, чтобы их сравнить и найти между ними что-то общее.

Напишу понятнее. Я знаю что такое Covariance and Сontravariance. Ваше "определение" "Вариантности" сложное и запутывает читателя. Когда читаешь Ваше "определение" возникает впечатление, что автор сам плохо понимает то о чем пишет.

IMHO Ваше определение "Вариантности" можно значительно улучшить.

Возможно есть смысл не использовать слово "Вариантность". А использовать Covariance and Сontravariance.

автор сам плохо понимает то о чем пишет.

IMHO Ваше определение "Вариантности" можно значительно улучшить.

А вот я в вас верю. Не держите в себе, пишите, улучшайте!

А если считать действительно сумму, без инициализации аккумулятора нулем? Если правильно понимаю, вот эта строчка
return changes.reduce(0, (a, b) -> Math.max(0, a + b));
приводит к тому, что для списка (a,b,c,d...) мы считаем на самом деле 0 ⊕ a ⊕ b ⊕ c ⊕ d +… (вместо a ⊕ b ⊕ c ⊕ d + ...). Благодаря этому функция ⊕ у нас применяется всегда для неотрицательного первого числа, отсюда например и
⊕a ⊕b = ⊕(a+b)
хотя в общем случае x ⊕ 1 ⊕ 2 != x ⊕ 3

Да, если разрешить начальному значению быть отрицательным — то ⊕a ⊕b перестаёт редуцироваться до ⊕(a+b) и всё рассыпается.


Но решение тут простое — надо разделить исходный список на "голову" и "хвост":


S ⊕ a ⊕ b ⊕ c ⊕ d +… = (S ⊕ a) ⊕ b ⊕ c ⊕ d +…


Здесь S ⊕ a всегда неотрицательное, а значит для хвоста можно использовать обсуждаемый алгоритм.


Точно так же решается задача и для случая, когда у нас вообще нет никакого аккумулятора, только теперь придётся выделить уже два элемента из головы списка:


a ⊕ b ⊕ c ⊕ d +… = (a ⊕ b) ⊕ c ⊕ d +…

Возможно, @mayorovp меня поправит, что он имел в виду. Я за круглым плюсом ⊕ вижу 2 смысла. Чтобы их не путать, введём квадратный плюс ⊞.

  • ⊖fall отрицательный морфизм;

  • ⊕grow положительный морфизм;

  • a ⊞ b = max(0, a + b) = relu(a + b), неудобная неассоциативная бинарная операция;

  • кусок истории ((((x ⊞ a) ⊞ b) ⊞ c) ⊞ …) представим смесью морфизмов, парой (⊖fall, ⊕grow).

Да, считаем (((0 ⊞ a) ⊞ b) ⊞ c) ⊞ …, как будто начинаем с пустого склада. Не забываем про неявные скобочки левой свёртки.

В общем случае верны оба:

  • (x ⊞ 1) ⊞ 2 = x ⊞ 3. Но неассоциативно.

  • (⊕1)∘(⊕2) = (⊕3). Ассоциативно и в длинных скобки можно выражениях скобки можно опускать ⊕a∘⊕b∘⊕c = ⊕(a+b+c). Но представление не полно, нужны ещё отрицательные морфизмы , для записи произвольной истории нужна пара (⊖fall, ⊕grow).

Фух! Сложно объяснить простой код :(

Вам говорят, что (x ⊞ 1) ⊞ 2 не всегда равно x ⊞ 3. К примеру, (-2 ⊞ 1) ⊞ 2 = 2, в то время как -2 ⊞ 3 = 1.


Если начинать с пустого или хотя бы валидного склада — это не играет роли, но если начать со склада в невалидном состоянии — алгоритм отработает некорректно. Является ли это ошибкой — другой вопрос. Для случая со складом — наверное, нет. В общем случае — наверное, да.

Пример с x=-2 понятен, действительно, в этом случае будут проблемы, но не у grosum. Дальше зависит от договорённостей, что такое общий случай :) Я предполагаю fall и grow неотрицательными, как у Гротендика, соответственно, x не может быть -2. В посте я ухожу даже от плавающей точки, чтобы не касаться неассоциативности при округлении. Опущены даже вопросы переполнения long. Множество других вопросов остаются на совести разработчика приложения.

Но алгоритм из поста, grosum [-2, 1, 2] = 3 = grosum [1, 2] = grosum [3]. Алгоритм, который вернёт 2 или 1 — это некий другой, модифицированный алгоритм. Прежде чем рассуждать о нём, сначала необходимо зафиксировать его код.

Вам же с самого начала написали:


А если считать действительно сумму, без инициализации аккумулятора нулем?

Разумеется, grosum, считающая что в аккумуляторе настолько ноль, что его даже нет в алгоритме, для этого случая напрямую не подходит.


Вот вам обсуждаемая задача, если до сих пор не понятно:


changes.reduce((a, b) -> Math.max(0, a + b));

А если считать действительно сумму, без инициализации аккумулятора нулем?

Без примера кода точный смысл этого высказывания мне не ясен: инициализировать его чем-то другим, чтобы что сделать? Какой тезис обсуждаем?

Знаю только, что в неотрицательной сумме нет отрицательных чисел ни в начальном балансе S, ни в промежуточном балансе (в том числе в S⊞a), ни даже в названии :) А из ложной посылки (S отрицательно) можно вывести всё, что угодно, так что заранее согласен.

Вот вам пример:


changes.reduce((a, b) -> Math.max(0, a + b));

Что тут непонятно-то? Как распаралелить этот код?

Этот код не полон. Перед тем, как параллелить, сначала типы поправьте: результат должен быть числом, что делать с OptionalLong, почему и зачем?

Был код, который решал задачу, вы его сломали и спрашиваете как починить? Не знаю, что может быть лучше, чем откатить коммит :) Вы хорошо разбираетесь в вопросе — ваш первый комментарий это показал. Уверен, вы правы, только я не понимаю постановку задачи, которую вы решаете — она у вас в голове. Ветка дискуссии разрослась, поэтому прошу в личку.

Чего именно тут не хватает?


что делать с OptionalLong

вернуть


почему и зачем

потому что задача такая

Спасибо, было интересно, но почему работает – не понимал, пока не вывел алгоритм сам. Привычней всё-таки идти по старинке, не от монад, а от инвариантов цикла.

Sign up to leave a comment.