Re: Никогда не недооценивайте силу по умолчанию
От: mrTwister Россия  
Дата: 13.09.22 16:00
Оценка:
Здравствуйте, Caracrist, Вы писали:

C>

Это вообще ни к селу. В нормальных языках есть горутины или аналог и про синхронность или асинхронность думать вообще не надо.
лэт ми спик фром май харт
Re[2]: Никогда не недооценивайте силу по умолчанию
От: SkyDance Земля  
Дата: 13.09.22 23:32
Оценка:
T>Это вообще ни к селу. В нормальных языках есть горутины или аналог и про синхронность или асинхронность думать вообще не надо.

Как по мне так наоборот. Собственно, асинхронность (или concurrency в общем виде) как first class citizen и есть ключевое свойство современных языков. Я бы даже жестче выразился — языки, где асинхронность выражена через костыли (привет, С++ и Java), а не встроена в синтаксис языка (Hello Joe), — устарели лет уже 10 как.

Проблема как раз в том, что про синхронность или асинхронность нужно очень много думать — см. вопросы про примитивы синхронизации на собеседованиях.
Re[6]: Никогда не недооценивайте силу по умолчанию
От: Sinclair Россия https://github.com/evilguest/
Дата: 14.09.22 04:34
Оценка:
Здравствуйте, netch80, Вы писали:

N>(Property стиля C# решают это же косвенно, но ломая ABI для тех модулей, что уже скомпилировались с прямым доступом. Или там это обошли?)

Нет, не обошли. В шарпе (и дотнете вообще) есть очень немного способов подменить реализацию зависимости без перекомпиляции зависимого.
Правила вычисления этих способов настолько нетривиальны, что стандартной является рекомендация "Всегда перекомпилируйте A при перекомпиляции B, если A хоть что-то импортирует из B".
Начиная с public const, которые просто вхардкоживаются в использующий код, и заканчивая всякими disambiguation rules.

N>2. А ещё важнее то, что если у тебя в публичном доступе есть только методы, сохраняющие инварианты, а прямая установка поля этот инвариант может сломать, то выставление в public только беспроблемных методов реально повышает защиту.

+10.
N>Да, это азы, и я надеюсь, что объяснять их не надо было, но это необходимо как вводная к тому, что чем меньше автоматического public, тем лучше для сохранности.
Вообще, по моему опыту, как раз есть исчезающе мало случаев, когда состояние объекта можно менять без его ведома. Всегда есть какие-то неожиданные инварианты — кроме примитивных value-типов.
Ну, там, какой-нибудь "класс" Point с целыми X и Y.

N>Кстати, я таки думаю, что самый правильный стиль из этих таки не в C++ private/protected/public, а в Java/C#, где умолчанием является package-internal. При нём и посторонние не пролезут, и круг своих аккуратно ограничен, но достаточно широк, чтобы не кидаться friendʼами попусту.

+
N>Где 95% случаев и почему?
N>Вокруг меня всё-таки 95% случаев это когда класс имеет своё состояние, которое надо защищать даже от случайных диверсий поломки целостности.
Подозреваю, что речь идёт о какой-нибудь примитивной математике над структурами и массивами.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[3]: Никогда не недооценивайте силу по умолчанию
От: Sinclair Россия https://github.com/evilguest/
Дата: 14.09.22 04:39
Оценка:
Здравствуйте, SkyDance, Вы писали:
SD>Проблема как раз в том, что про синхронность или асинхронность нужно очень много думать — см. вопросы про примитивы синхронизации на собеседованиях.
Вот это — интересная концепция.
Если у нас есть async-метод, написанный в терминах await — есть ли способ по нему автоматически породить синхронную версию?

Сходу мне это кажется сомнительным с практической точки зрения.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[4]: Никогда не недооценивайте силу по умолчанию
От: SkyDance Земля  
Дата: 15.09.22 16:55
Оценка:
S>Если у нас есть async-метод, написанный в терминах await — есть ли способ по нему автоматически породить синхронную версию?

Дело в том, что "синхронности" в реальном мире не существует. Это абстракция, появившаяся в силу ограничений человеческого восприятия (что-то очень быстрое воспринимается мгновенным, а два таких события — последовательным).

Синхронный вызов есть ни что иное как жесткая связка call + return. Стало быть, два асинхронных сообщения: первое — call, второе — return. В некоторых реализациях (называемых directly threaded) вторая инструкция, return, называется continue, и адрес, с которого следует продолжить исполнение — continuation pointer.

Как только мозг осознает данную концепцию, так сразу становится легко понять, почему все методы всегда по природе своей асинхронны. Синхронность нужна лишь для того, чтобы упростить логическое доказательство корректности (что называется, easier to reason about). По сути это запрет на любые действия между отправкой сообщения "call" и вечным (то есть без таймаута) ожиданием ответа (return).
Re[8]: Никогда не недооценивайте силу по умолчанию
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 16.09.22 06:02
Оценка:
Здравствуйте, vsb, Вы писали:

vsb>Можно делить на ошибки и предупреждения, но в релизном коде не должно быть ни тех, ни других. Хотя я бы вообще предложил кардинальный подход — не делить на ошибки и предупреждения, а генерировать в отладочном режиме бинарник, даже если встречаются ошибки, которые можно локализовать. Я давным давно писал на жава в эклипсе, вот там такая фича была. И это было очень удобно. Не дописал функцию, и ладно. Если туда управление придёт, то вместо недокомпилированного кода вылетит ошибка.


Прикольно. Для этого требовался особый JDK, или Eclipse внутри себя это делал?

vsb>Что касается автоматической генерации кода, на мой взгляд правильно требовать от генератора генерировать нормальный код, а не подстраивать язык под него.


Вот в том, что такое "нормальный" код, тут и есть засада. Go не пропускает, например, код с неприменённым импортом. Генератор должен следить за тем, вызвал он что-то по этому импорту или нет? Я думаю, что не должен. Даже если по умолчанию это полезно (Go рассчитывался на написание кода низкоуровневыми ширнармассами, и жёсткий контроль имеет смысл), опция компиляции для убирания этого (вписанная в исходник) была бы очень полезна.

N>>Надо смотреть на практику Питона, где эта диверсия по умолчанию.


vsb>Вот, кстати, да. Я на питоне не то, чтобы много писал, но немного писал. И конкретно эта фича мне проблем кажется не доставляла.


А мне доставляла — и нетипизированностью, и тем, что сложно понимать, определена переменная в принципе или нет.

vsb>>>Автовывод типа это уже давно стандарт де-факто почти во всех языках и подразумевается сам собой.


N>>Да вот не совсем. Ты смешиваешь два автовывода — автовывод по инициализации и автовывод по всему использованию.

N>>Первый — удел процедурных языков. Второй — группы *ML/Haskell/etc.

vsb>Я про первый. Автовывод типов для функций — вопрос сложный.


Не обязательно для функций.

r = 0;
... какие-то действия в цикле... {
++r;
}

Если оно умеет подсчитать, что r достигает 2000, и 1 байта мало, нужно два — это и есть второй вариант.

vsb>>>Все типы локальных переменных выводятся и это не опционально.


N>>Ну да, ещё скажи, что ты не можешь сказать "int x" для локальной переменной, только "var x"

N>>Непонятно, что ты имел в виду на самом деле, но сказано безнадёжно коряво. Перефразируй.

vsb>Не очень понял, что не понятно. У нас ведь в гипотетическом языке нет ни var ни int. У нас есть только x = 1 или x = get_int_value(). Иными словами типы всех локальных переменных выводятся автоматически и отказаться от этого нельзя. Максимум — добавить каст в инициализирующее выражение, чтобы привести его к нужному типу.


Значит, таки типы есть, раз касты есть. Но зачем тогда запрещать тип для переменной?
The God is real, unless declared integer.
Re[2]: Никогда не недооценивайте силу по умолчанию
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 16.09.22 06:05
Оценка:
Здравствуйте, mrTwister, Вы писали:

C>>

T>Это вообще ни к селу. В нормальных языках есть горутины или аналог и про синхронность или асинхронность думать вообще не надо.


На чём будет написан рантайм Go, реализующий шедулер горутин, и почему этот язык не является нормальным?
The God is real, unless declared integer.
Re[9]: Никогда не недооценивайте силу по умолчанию
От: vsb Казахстан  
Дата: 16.09.22 06:24
Оценка:
Здравствуйте, netch80, Вы писали:

vsb>>Можно делить на ошибки и предупреждения, но в релизном коде не должно быть ни тех, ни других. Хотя я бы вообще предложил кардинальный подход — не делить на ошибки и предупреждения, а генерировать в отладочном режиме бинарник, даже если встречаются ошибки, которые можно локализовать. Я давным давно писал на жава в эклипсе, вот там такая фича была. И это было очень удобно. Не дописал функцию, и ладно. Если туда управление придёт, то вместо недокомпилированного кода вылетит ошибка.


N>Прикольно. Для этого требовался особый JDK, или Eclipse внутри себя это делал?


У эклипса был свой компилятор. Ну и сейчас есть, наверное. Вроде как его даже к идее можно как-то присобачить, но я не пробовал.


vsb>>Что касается автоматической генерации кода, на мой взгляд правильно требовать от генератора генерировать нормальный код, а не подстраивать язык под него.


N>Вот в том, что такое "нормальный" код, тут и есть засада. Go не пропускает, например, код с неприменённым импортом. Генератор должен следить за тем, вызвал он что-то по этому импорту или нет? Я думаю, что не должен. Даже если по умолчанию это полезно (Go рассчитывался на написание кода низкоуровневыми ширнармассами, и жёсткий контроль имеет смысл), опция компиляции для убирания этого (вписанная в исходник) была бы очень полезна.


Я согласен, что во время разработки можно пропускать максимально много ошибок, пока компилятор уверен, что программа остаётся адекватной. И эта особенность го мне тоже не нравится.


N>Не обязательно для функций.


N>r = 0;

N>... какие-то действия в цикле... {
N> ++r;
N>}

N>Если оно умеет подсчитать, что r достигает 2000, и 1 байта мало, нужно два — это и есть второй вариант.


Это интересный вопрос, что делать с int-ами. В целом я бы хотел, чтобы по умолчанию оно работало автоматически, сначала 4 байта (по-моему меньше — смысла нет), если переполнились, то автоматически превращало в 8, потом в безразмерное целое. Но надо подумать над этим вопросом. Понятно, что если язык претендует на хоть какую-то производительность, такое может быть не реализуемо.

vsb>>>>Все типы локальных переменных выводятся и это не опционально.


N>>>Ну да, ещё скажи, что ты не можешь сказать "int x" для локальной переменной, только "var x"

N>>>Непонятно, что ты имел в виду на самом деле, но сказано безнадёжно коряво. Перефразируй.

vsb>>Не очень понял, что не понятно. У нас ведь в гипотетическом языке нет ни var ни int. У нас есть только x = 1 или x = get_int_value(). Иными словами типы всех локальных переменных выводятся автоматически и отказаться от этого нельзя. Максимум — добавить каст в инициализирующее выражение, чтобы привести его к нужному типу.


N>Значит, таки типы есть, раз касты есть. Но зачем тогда запрещать тип для переменной?


Типы есть, в том числе типы по текущей концепции обязательны у функций. Зачем запрещать? Потому, что два способа написать одно и то же это плохо. И когда нет формального способа решать, какой способ лучше, это плохо. Любая отсылка к какому-нибудь эстетическому чувству программиста, мол он сам выберет — ставить тип или нет, это плохо, это ненадёжно. Можно считать эту концепцию антиподом философии Perl ("всегда есть более одного способа написать это").

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

Если вернуться к int-ам в этой концепции, то можно добавить для них возможность указания нестандартного размера. К примеру по умолчанию int32, но если это умолчание не подходит, то можно указать другой размер (при этом int32 указать нельзя). Хотя я бы предпочёл для этого юз-кейса синтаксис вроде x = (int8) 0, от кастов-то всяко никуда не деться.
Отредактировано 16.09.2022 6:29 vsb . Предыдущая версия .
Re[5]: Никогда не недооценивайте силу по умолчанию
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 16.09.22 06:27
Оценка: +1
Здравствуйте, SkyDance, Вы писали:

S>>Если у нас есть async-метод, написанный в терминах await — есть ли способ по нему автоматически породить синхронную версию?


SD>Дело в том, что "синхронности" в реальном мире не существует. Это абстракция, появившаяся в силу ограничений человеческого восприятия (что-то очень быстрое воспринимается мгновенным, а два таких события — последовательным).


Это всё хорошо, но для компьютера непрактично, потому что ему, наоборот, синхронность естественна, а асинхронность требует специального дизайна.
По крайней мере пока у него фоннеймановская архитектура (включая параллельные версии).
The God is real, unless declared integer.
Re[4]: Никогда не недооценивайте силу по умолчанию
От: Ночной Смотрящий Россия  
Дата: 16.09.22 18:02
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Если у нас есть async-метод, написанный в терминах await — есть ли способ по нему автоматически породить синхронную версию?


Только GetAwaiter().GetResult(), что смысла особого не имеет. А по честному выпрямить когда у тебя все ранво все упрется в библиотечные методы, возвращающие Task или overlapped IO — без шансов.
... << RSDN@Home 1.3.17 alpha 5 rev. 62>>
Re[6]: Никогда не недооценивайте силу по умолчанию
От: SkyDance Земля  
Дата: 16.09.22 18:26
Оценка:
N>По крайней мере пока у него фоннеймановская архитектура (включая параллельные версии).

Только если рассуждать в пределах одного ядра вычислений. Если взять абстракции более высокого уровня (distributed computing), то уже не все так очевидно.

На самом деле было бы интересно посмотреть на язык, с самого начала заточенный на scheduling методов в глобальном пространстве. Не как горутины/fibers/green threads, в пределах одного компьютера, а — в глобальном распределенном кластере. Попыток дофига (всякие там RPC), но все это делается без изменения семантики, просто к функции добавляется еще один return code "не шмогла", или, еще хуже, timeout.
Re[5]: Никогда не недооценивайте силу по умолчанию
От: Sinclair Россия https://github.com/evilguest/
Дата: 17.09.22 04:37
Оценка:
Здравствуйте, Ночной Смотрящий, Вы писали:
НС>Только GetAwaiter().GetResult(), что смысла особого не имеет. А по честному выпрямить когда у тебя все ранво все упрется в библиотечные методы, возвращающие Task или overlapped IO — без шансов.
То есть только с помощью словаря, который сопоставляет асинк методам их синхронную версию.
В дотнете это, наверное, бессмысленно — синк и асинк слишком по-разному работают.
К примеру, синхронную версию платформенного апи можно вызывать со Span<byte>, что офигенно эффективно и во многих сценариях позволяет сократить вмешательство GC.
А в асинхронной версии так сделать нельзя — надо тащить полноценные Memory<T> или ещё какие-то аналоги, живущие в хипе.
То есть даже если мы автоматически породим синхронную версию кода, и заменим вызовы overlapped io на синхронные, то общее решение будет далеко от идеала.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[5]: Никогда не недооценивайте силу по умолчанию
От: maxkar  
Дата: 19.09.22 13:09
Оценка: 102 (2)
Здравствуйте, SkyDance, Вы писали:

SD>Дело в том, что "синхронности" в реальном мире не существует. Это абстракция, появившаяся в силу ограничений человеческого восприятия.

Синхронность в реальном мире как раз существует. Для человека оно соответствует "фокусу внимания" (еще в литературе иногда встречается локус внимания/locus of attention). То, что попадает в фокус — синхронно. То, что не попадает — асинхронно. У меня по этому поводу есть иллюстрация из моих слайдов по монадам. Допустим, наша задача — научить друга делать торт по определенному рецепту. Это можно делать разными способами. Например, вы можете давать инструкции, наблюдать за процессом выполнения и потом давать новые инструкции. С вашей точки зрения данное исполнение синхронно (у вас внимание сконцентрированно на друге и его действиях). А можно дать ему инструкцию вида "взбивай белки, когда закончишь — позвони, я дам следующую инструкцию". В этом случае выполнение для вас асинхронно, вы можете переносить внимание на другие операции.

Из примера выше следует интересное наблюдение. Люди неплохо умеют оперировать с абстракциями высокого порядка (higher kinded polymorphism)! Рецепт, записанный на бумаге ("взбить яйца, добавить муки, ...") является именно таким высокоуровневым алгоритмом. Два варианта выполнения с использованием друга я привел выше. Можно выполнять процесс самому. А мастер-класс по приготовлению вообще будет выполнением в стиле SIMD! Общее во всех этих сценариях не синхронность или асинхронность, общая часть — это последовательное выполнение (sequential execution).

Что еще можно сказать про людей? Понятие "асинхронности" настолько распространено, что для его выражения существуют специальные языковые конструкции. В русском языке это деепричастия и деепричастные обороты. "Тихонько напевая, Наташа взбивала белки". Здесь очень явный фокус на основное (взбивание) и асинхронное/побочное (напевание) действия. Есть и более параллельный вариант: "Наташа громко пела Интернационал и яростно взбивала белки". Как видите, никаких проблем с асинхронностью на самом деле нет. Как минимум — на уровне независимых высокоуровневых действий.

А вот где проблемы есть, так это в одновременном доступе к ресурсам. По своей природе человек привык к эксклюзивному владению ("распоряжению") в своей работе. Люди не умеют работать в условиях высокой конкуренции (contention), в этом случае обычно возникает конфликт за владение и кто-то выходит победителем. Да и конкуретный доступ обычно заметен сразу. Например, если кто-то взял вашу палку-копалку, вы не увидите ее на привычном месте. А в программировании вы ее найдете, но вести она себя будет совсем по-другому.

Есть и некоторые проблемы при координации действий нескольких людей. Ну например, охота на мамонта (пойдут и любые другие групповые действия). Здесь у нас нет конфликтов (contention) по владению, но может быть взаимовлияние (interference). Традиционное эволюционное решение — создание иерархии. Появляется руководитель (лидер, дирижер и т.п.), который координирует остальную команду. Он в некоторой степени знает, что делает каждый участник. Что само по себе предоставляет еще одну проблему — здесь нет инкапсуляции! Люди не умеют разбивать задачу на "цели", они разбивают ее на "последовательности координированных действий". А эта модель не очень хорошо ложится в бытовые представления об асинхронности.

Т.е. можно выделить следующие проблемы асинхронности:

SD>Синхронный вызов есть ни что иное как жесткая связка call + return.

Это спорный вопрос. Здесь есть противоречие между различными уровнями абстракций. Есть "логическая" операция, и есть "физическая". Например, "запечь курицу" состоит из "поставить курицу в духовку" (синхронное действие) и "дождаться готовности" (асинхронное). В программах это различие тоже может быть важным. Например, мы хотим предложить пользователю магазина товары, которые он уже покупал. Для этого нам нужно загрузить данные о предыдущих покупках и инвентарь (чтобы не предлагать товары, которых нет в наличии). В этом случае очень хочется явно различать начало и возврат:
val ordersOp = userService.getRecentOrders(user.id); //Async[Seq[Orders]]
val inventoryOp = inventoryService.getCurrentInventory(); //Async[Inventory]

val orders = await ordersOp;
val inventory = await inventoryOp;

val items = orders.flatMap(_.items).filter(item => inventory.getCount(item.id) > 0);

И опять же, наблюдается разница между последовательностью (сначала начинаем загружать покупки, потом — инвентарь) и асинхронность (обработка логических результатов операции). Считать, что getRecentOrders асинхронно возвращют асинхронный результат — можно. Но вряд ли это очень удобно.

SD>Синхронность нужна лишь для того, чтобы упростить логическое доказательство корректности (что называется, easier to reason about).

Во многих случаях нужна именно последовательность, не синхронность. Синхронность выполнения здесь ничем не помогает. Если у нас есть несколько потоков (и нет явного владения), то у нас могут возникать гонки (race). А если всего один "поток выполнения", то гонок нет. При этом вполне может быть "асинхронное последовательное выполнение". Например, среда выполнения может вместо блокирующих вызовов IO внутри делать неблокирующие и прекращать интерпертацию программы до лучших времен.

И еще один пример "последовательного" вместо "синхронного". Классический javascript (который однопоточный) и его promises против for comprehension/do notation. Вложенные then читать гораздо сложнее, чем for. Сравните:

let ordersOp = userService.getRecentOrders(user.id); //Async[Seq[Orders]]
let inventoryOp = inventoryService.getCurrentInventory(); //Async[Inventory]

ordersOp.then(orders => 
  inventoryOp.then(inventory =>
    let items = ...
  )
)

или
for {
  orders <- userSerivce.getRecentOrders(user.id)
  inventory <- inventoryService.getCurrentInventory()
  items = ...
} yield {
  ...
}


Код один и тот же. Но в одном случае он выглядит последовательным, а в другом — нет.

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

> языки, где асинхронность выражена через костыли

Вот исходя из вышеизложенного, она выражена через костыли почти везде! Я не зря выше привел деепричастия в качестве примера. Где они в языках, где асинхронность поддерживается из коробки? Там есть специальная конструкция для "ожидания" завершения действия, которая в естественных языках выражается глаголами ("дождитесь") или наречиями ("после", "затем"). А вот как "естественно" сделать из синхронной операции асинхронную (т.е. породить деепричастие из глагола) в современных языках? А никак. Задача обычно сводится к вызову других глаголов из библиотек (какой-нибудь threadPool.execute). Получаетя асимметрия между созданием асинхронного вычисления (обычные глаголы в ЯП, нет встроенной конструкции, в естественных языках — специальная конструкция) и ожиданием (специальная конструкция в ЯП, обычные глаголы или наречия в естественных языках). Более-менее симметричный пример я могу только из Scala привести:
val x = Future { doSomething() }
x.flatMap { v => ...
}

Здесь Future делает асинхронное вычисление из "глагола" (действия в фигурных скобках). Здесь и создание, и использование — обычные вызовы методов. Специальные конструкции языка не используются.

Хотя вообще специальный синтаксис языка полезен. Но не привязанный к асинхронности, а привязанный к последовательности (ага, монады). Это уже упомянутые for comprehension (scala) и do-notation (haskell). Эта необходимость вызвана тем, что наречие "затем" по-разному работает на микро- и макро-уровнях. На микро-уровне: "Приготовьте пирог, затем подайте его" — операция подразумевает, что результат одного шага доступен на следующем. Это удобно для операций вида "приготовить пирог" — они должны всего-лишь заботиться о том, как передать свой непосредственный результат. К сожалению, на макроуровне у нас есть "Приготовьте пирог, затем сварите суп, затем подавайте все на стол". К моменту подачи на стол у нас доступны результаты всех предыдущих операций (и пирог, и суп) а не только последней (суп). Явно протаскивать контекст через внутренние операции — неудобно и неестественно (зачем нам в операции "сварить суп" этот внешний контекст в виде пирога?). Переписывание for как раз устраняет разницу в семантиках. На микро-уровне (затем/flatMap/bind/then) так и остается на индивидуальном уровне. А на верхнем уровне компилятор правильно протаскивает накапливающийся контекст в последующие операции.

do notation и for comprehension — хороший инструмент, но очень ограниченный. К сожалению, он работает только с последовательным выполнением. Ну и ветвление можно более-менее безболезненно сделать. А вот циклы приходится вручную через какие-нибудь fold или рекурсию расписывать. Вот хорошо бы, чтобы компилятор и управляющие конструкции тоже умел преобразовывать (я готов от return отказаться ради такого, if/while вроде бы нормально сводятся к вызовам методов). С учетом деепричастий, что нибудь вида
for[M] def getTopOffers(user: User): Seq[Item] = {
  val userOrdersOp = par userService.getRecentOrders()
  val inventory = inventoryService.getCurrentInventory()
  val userOrders = userOrdersOp

  val items = orders.flatMap(_.items).filter(item => inventory.getCount(item.id) > 0);
  items
}


Как это работает. У нас есть монада M (параметр — конструктор типа). По-умолчанию любое выражение (в том числе — внутри другого выражения) типа M[T] преобразуется в monad.bind(e, { t: T => ...} (или соответствующий эквивалент для циклов). Любое другое выражение — остается тем, которым есть. Выражения, помеченные par не преобразуются в bind (по сути, par — это деепричастный оборот из существующей операции). Т.е. пример выше эквивалентен (аннотации типов добавлены для ясности)
def getTopOffers[M: Monad](user: User): Seq[Item] = {
  val userOrdersOp: M[Seq[Order]] = userService.getRecentOrders()
  for {
    (inventory: Inventory) <- (inventoryService.getCurrentInventory(): M[Inventory])
    (userOrders: Seq[Order]) <- userOrdersOp
    (items: Seq[Item]) = orders.flatMap(_.items).filter(item => inventory.getCount(item.id) > 0)
  } yield 
    items
}

Оно еще потом рассахаривается (за несколько шагов) в ужасы вида
def getTopOffers[M](user: User)(implicit md: Monad[M]): Seq[Item] = {
  val userOrdersOp: M[Seq[Order]] = userService.getRecentOrders()
  
  md.bind(
    inventoryService.getCurrentInventory(),
    inventory => {
      md.mmap(userOrdersOp),
      userOrders => {
        va items = orders.flatMap(_.items).filter(item => inventory.getCount(item.id) > 0)
        items
      }
    }
  )
}

Потом для синхронного случая (Identity) компилятор может еще все заинлайнить и получить обычный последовательный код.

Вот так мог бы выглядеть современный язык программирования. Он поддерживает последовательность, абстрагируется от нижележащего выполнения. Конструкция par позволяет нарушить "последовательное" (конец-начало) выполнения и получить "параллельную" операцию (без спецификации конкретного механизма выполнения). И вот в данном случае "par" будет ближе к естественным языкам.

P.S. Извините за длинный пост. Часть из него — для меня. Я наконец-то начал понимать, чего именно я хочу от абстракций над монадами .
Re[7]: Никогда не недооценивайте силу по умолчанию
От: maxkar  
Дата: 19.09.22 14:16
Оценка: 105 (1)
Здравствуйте, SkyDance, Вы писали:

SD>На самом деле было бы интересно посмотреть на язык, с самого начала заточенный на scheduling методов в глобальном пространстве. Не как горутины/fibers/green threads, в пределах одного компьютера, а — в глобальном распределенном кластере. Попыток дофига (всякие там RPC), но все это делается без изменения семантики, просто к функции добавляется еще один return code "не шмогла", или, еще хуже, timeout.


У меня есть пара примеров . Язык для координации различных REST-сервисов в кластере. Дополнительная цель — вынести кастомизацию поведения из севрисов (feature flags) на уровень координатора.

Первый пример (на уровне оркестратора всей системы):
def makePayment(order):
  val paymentClass = order.vendor == '123' ? 'Reliable' : 'Cheap'
  val paymentId = newUUID()
  val paymentRequest = Payment(
    id = paymentId,
    currencies = ...,
    amount = ...,
    customer = ...,
    paymentClass = paymentClass,
    items = order.items
  )
  order.state = Order.PAYMENT_IN_PROGRESS
  order.payments.push(paymentRequest)
  notify paymentSvc of paymentRequest
  wait paymentRequest in paymentSvc to { p ->
    p.status != Payment.AUTHORIZING
  }
  updatemPayment.status match {
    case Payment.REDIRECT => // redirect to URL...
    case Payment.REJECTED => // handle rejected status,
  }

Обработка платежа заказа. На основе вендора определяется, что нам важнее в обработке заказа (чтобы вынести эти проверки из payment service). Создаем платеж. Добавляем платеж к заказу. Затем уведомляем REST payment service о том, что у нас есть новая entity. Среда обеспечивает надежность (повторные запросы при необходимости, для этого мне нужна идемпотентность — REST). Затем мы ожидаем, пока пройдет "промежуточное" состояние Authorizing (в сервисе могут быть свои повторы, таймауты и прочая внешняя интеграция). Уведомление об изменениях — через очередь событий (event queue). Что-то банальное вроде "payment id = ... changed", остальное можно вытянуть через rest зная id. После обновления выполнение продолжается дальше.

Второй пример. Координация действий в рамках одной entity (на стороне payment service)!:
def cancelPayment(my payment, cancelRequest):
  payment.cancelRequests.push(cancelRequest)
  wait payment in self to { p ->
    p.authorization.state != Auth.AUTHORIZING
  }
  payment.authorization.state match {
    case Auth.REJECTED | Auth.CANCELLING | Auth.CANCELLED => 
      cancelRequest.status = Cancel.IGNORED
    case Auth.ACCEPTED =>
      cancelRequest.status = Cancel.IN_PROGRESS
      payment.status = Payment.CANCELLING
      native callExternalService(payment)
  }

Иллюстрирует ситуацию, когда у нас есть промежуточное состояние, в котором нельзя дальше двигаться. Например, отмена платежа зависит от того, авторизован он или нет. Метод регистрирует новый запрос. Затем ждет, пока у нас появится определенность в состоянии. Затем в зависимости от того, как прошла авторизация, выполняем следующие действия. Если авторизация не прошла — ничего делать не надо, только отметить в базе. Иначе нужно поставить промежуточное состояние и вызвать внешний сервис. Здесь native — это "метод расширения" платформы. Например, ручная реализация повторов и отказоустойчивости для внешнего (вендорского) RPC-сервиса. Все выполнение надежно. Если вдруг сервис, выполняющий код, упал — другая машина может взять и продолжить выполнение (с native есть некоторые сложности, но в целом ничего нерешаемого). В языке можно поддерживать все нормальные конструкции (ветвление, циклы, методы).

Что ещё. Условная "транзакционность" и отсутствие гонок (только в рамках entity, write skew я не планирую решать). Вот у нас есть makePayment. В обычной среде во время выполнения (которое ставит PAYMENT_IN_PROCESS) что-то другое может изменить платеж (например, поставить CANCELLED) и будет гонка. В типичном случае это будет http 500 с исключением OptimisticLockingException (хоть кто-нибудь бы это обрабатывал!). А здесь не будет. Гарантируется, что нет конкуретного доступа в участках между notify/wait/native. Это в реализации достаточно просто. У нас есть safe и unsafe инструкции. Safe не содержит доступа к внешним сервисам (но может манипулировать внутренним состоянием). Во время выполнения мы используем ORM (не люблю их, но для задачи подходит). При native/notify/wait пытаемся сохранить (транзакционно) состояние enitity и стек вычисления. Если получилось — продолжаем unsafe операцию, после нее перечитываем entity. Не получилось — начинаем операцию заново с момента предыдущего сохранения.

Таким образом, у нас есть:

Из языка у меня пока только concept-art Первую среду выполнения (runtime) для опробования идей я оцениваю в 6-9 месяцев полной занятости. Т.е. через это время на "низкоуровневом ассемблере" можно будет писать программы, запускать их и вообще пытаться решать реальные задачи координации (то, что сейчас решается API Gateway). А потом еще несколько лет на плюшки вроде высокоуровневого языка, типизации, просмотрщика и отладчика workflow в браузере. Но вообще мне пока нужны безумцы, которые хотели бы попробовать эту идею в реальных системах. Не хочу заниматься задачей без обратной связи.
Re[6]: Никогда не недооценивайте силу по умолчанию
От: SkyDance Земля  
Дата: 23.09.22 05:01
Оценка: 24 (1)
Отлично написано!
Получил истинное удовольствие, читая вашу заметку. Согласен с большинством утверждений. Особенно с этим:

M>Во многих случаях нужна именно последовательность, не синхронность.


В значительной степени это ровно то, что я и имел в виду. В concurrent programming это называют linearizability. Но для человеческого мозга это понятие слишком сложное, и еще сложнее становится манипулировать чем-то, что нас с самого детства учат делать последовательно: читать текст (программы или книги). Неспроста те самые деепричастия считаются довольно продвинутым элементом писательского мастерства. Читательского, однако, тоже — попробуйте прочитать текст со вложенными деепричастиями в сочетании со сложносочиненными и сложноподчиненными предложениями. Я как вспомню некоторые куски "Униженные и оскорбленные" — реальная тренировка для юношеского парсера. Порой приходилось по три раза перечитывать предложение, чтобы понять его смысл. В каждом конкретном предложении есть linearizability, но нет той самой синхронности. На конкретном примере:

Поровнявшись с кондитерской Миллера, я вдруг остановился как вкопанный и стал смотреть на ту сторону улицы, как будто предчувствуя, что вот сейчас со мной случится что-то необыкновенное, и в это-то самое мгновение на противоположной стороне я увидел старика и его собаку.


Сравниваем с этим:

Мартин понял, что сейчас ему не помешает лишняя чашечка кофе. Он дал организму мысленный зарок по возвращении на Землю неделю пить отвратительный бескофеиновый кофе и подошёл к стойке бара.


Может, конечно, это мои личные загоны, но на парсинг второго текста мне нужно на порядок меньше времени.

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

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

M>Вот так мог бы выглядеть современный язык программирования.


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

Мне бы хотелось какого-то более революционного подхода. Возможно, как-то решающего проблему "чтения сверху вниз слева направо" (и желательно без национальных особенностей типа RTL). Конкретных идей, увы, нет.

Если же оставаться в рамках существующих ограничений (программа — текст, читаем сверху вниз, слева направо), то я бы скорее видел это как машину состояний в примерно таком стиле:

initial() ->
    receive
        {accept, Request} -> accepted(Request);
        stop -> stopped()
    end.

accepted(Request) -> 
    Reply = process(Request),
    reply(Reply).

reply(Reply) ->
    send(Reply),
    initial().

process(Request) ->
    ask_for_payment_data(Request),
    ask_for_inventory_data(Request),
    multi_receive
        {Payment, Inventory} -> intersect(Payment, Inventory).


Это позволяет формализовать логику и значительным образом упростить тестирование (путем генерации тестов а-ля quickcheck/proper, с сохранением возможностей по shrinking'у).

Возможно, пример не самый удачный, но зато чуть менее concept art, и, пожалуй, при некотором упорстве реализуемый на Haskell'е. Но проблемы все те же, о которых вы написали — отсутствие условной транзакционности (более того, я вообще не представляю, как делать транзакционность в распределенных системах, даже на уровне банальных key-value storage — до сих пор прикидываю, есть ли какой-то способ надежно положить bidirectional map в этот самый key-value storage).

M>P.S. Извините за длинный пост. Часть из него — для меня. Я наконец-то начал понимать, чего именно я хочу от абстракций над монадами .


Напомнило вот это

Преподаватель другому говорит:
— "Вот студенты тупые, я им раз объясняю – не понимают, второй раз объясняю – не понимают, третий раз объясняю, уже сам понял, а они не понимают".


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


PS: а вопрос про feature flags надо бы еще раз продумать. Только не для REST сервисов (у них все ж есть удобные ограничения вроде идемпотентности запросов), а в более общем случае. Хотя бы для начала научиться атомарно переключать этот самый feature flag на целом кластере машин. Что-нибудь поумнее чем "флаг переключается ровно в 12:00, время синхронизируем по NTP" (хотя это тоже рабочий вариант, не завязанный на network availability в конкретный момент переключения).
Re[2]: Никогда не недооценивайте силу по умолчанию
От: x-code  
Дата: 26.09.22 05:33
Оценка:
Здравствуйте, Shtole, Вы писали:

S>Пожалуй, только const по умолчания хорош для всех типов языков.


А почему? Ведь в конечном итоге программирование — это работа с ячейками памяти, которая не только для чтения, но и для записи.
Пересоздавать каждый раз данные, особенно если это сложные и развесистые структуры данных — еще больший оверхед чем virtual.
Re[3]: Никогда не недооценивайте силу по умолчанию
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 26.09.22 08:42
Оценка: +1
Здравствуйте, x-code, Вы писали:

S>>Пожалуй, только const по умолчания хорош для всех типов языков.


XC>А почему? Ведь в конечном итоге программирование — это работа с ячейками памяти, которая не только для чтения, но и для записи.

XC>Пересоздавать каждый раз данные, особенно если это сложные и развесистые структуры данных — еще больший оверхед чем virtual.

1. Сказано же — по умолчанию, а не всегда

2. Для "сложных и развесистых структур данных" можно почитать книжку Окасаки "Чисто функциональные структуры данных" или посмотреть реализации, например, в Erlang.
Там этот вопрос решён для большинства типовых структур (деревья, хэш-таблицы и всё такое). Обновление меняет корневые ссылки и сохраняет основное тело структуры.
The God is real, unless declared integer.
Re[4]: Никогда не недооценивайте силу по умолчанию
От: x-code  
Дата: 26.09.22 09:49
Оценка:
Здравствуйте, netch80, Вы писали:

N>1. Сказано же — по умолчанию, а не всегда

N>2. Для "сложных и развесистых структур данных" можно почитать книжку Окасаки "Чисто функциональные структуры данных" или посмотреть реализации, например, в Erlang.
N>Там этот вопрос решён для большинства типовых структур (деревья, хэш-таблицы и всё такое). Обновление меняет корневые ссылки и сохраняет основное тело структуры.

Да можно, только вопрос в другом — что дает константность по умолчанию по сравнению с изменяемостью по умолчанию?
Re[5]: Никогда не недооценивайте силу по умолчанию
От: Sinclair Россия https://github.com/evilguest/
Дата: 26.09.22 17:55
Оценка: 4 (1)
Здравствуйте, x-code, Вы писали:
XC>Да можно, только вопрос в другом — что дает константность по умолчанию по сравнению с изменяемостью по умолчанию?
Массу всяческих гарантий.

Например, вы внутри своего кода можете полагаться на то, что никто не сломает данные, на которые вы ссылаетесь.
Допустим, некто А, пишет примерно вот такой класс (на воображаемом языке):
public class Order
{
  public List<OrderItem> Items {get; init;}
  
  public decimal Total { get; init;}
  public Order(List<OrderItems> items)
  {
    Items = items; 
    Total = items.Select(i=>i.LineTotal).Sum();
  }
}

Тут всё вроде бы ок. Items и Total — readonly.
Затем некто B пишет на основе класса, разработанного A, примерно такую программу:
...
var invoiceAmount = 0m;
List<OrderItems> orderItems;
foreach(var attachment in message.GetAttachments())
{
   orderItems.AddRange(ParseEDIOrder(attachment));
   var o = new Order(ediOrder.OrderLines);
   invoiceAmount += o.Total;
   orderCollection.Add(o);
   orderItems.Clear();
}
...

Затем QA отдел замечает, что происходит какая-то ерунда — в обработку уезжают пустые заказы с ненулевыми Total.
A, назначенный на починку своего класса Order, с ужасом понимает, что полученные им в конструкторе items почему-то изменяются без его ведома.
Ок, давайте попробуем это починить:
public class Order
{
  public List<OrderItem> Items {get; init;}
  
  public decimal Total { get; init;}
  public Order(List<OrderItems> items)
  {
    Items = new List<OrderItems>(items); 
    Total = items.Select(i=>i.LineTotal).Sum();
  }
}

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

Затем некто C добавляет в систему новую фичу — если сумма заказа больше 100, то надо сделать на него скидку 3%. Ну, это же очень просто — давайте добавим к order ещё одну позицию:
...
if (o.Total > 100)
   o.Items.Add(new OrderItem("Volume discount", -o.Total * 3/100);
...

Но увы — почему-то инвойс по-прежнему выписывается на сумму без скидки! QA заводит багу на A: "сумма заказа не совпадает с суммой его позиций"
A снова с ужасом понимает, что отданные им наружу Items почему-то изменяются без его ведома.
Ок, давайте попробуем это починить:
public class Order
{
  public IReadOnlyList<OrderItem> Items {get; init;}
  
  public decimal Total { get; init;}
  public Order(List<OrderItems> items)
  {
    Items = new List<OrderItems>(items); 
    Total = items.Select(i=>i.LineTotal).Sum();
  }
}


Теперь С обнаруживает, что его код обработки заказа перестал компилироваться.
Подсмотрев в код, он видит, что на самом деле в Order хранится List, поэтому пишет вот так:
...
if (o.Total > 100)
   ((List<OrderItem>)o.Items).Add(new OrderItem("Volume discount", -o.Total * 3/100);
...

И код начинает компилироваться. Но багу, закрытую А в предыдущем коммите, открывают заново.
Почесав репу, он делает вот так:

public class Order
{
  private List<OrderItem> _items;
  public IReadOnlyList<OrderItem> Items {get => new List<OrderItem>(_items); }
  
  public decimal Total { get; init;}
  public Order(List<OrderItems> items)
  {
    _items = new List<OrderItems>(items); 
    Total = items.Select(i=>i.LineTotal).Sum();
  }
}

Ну всё, теперь можно спать спокойно. Баг теперь в коде C, потому что его вызов Add ничего в заказ не добавляет. Правда, у нас тут клонирование списков направо и налево...


Например, вы внутри своего кода можете полагаться на то, что переданная вам ссылка на что-то продолжит показывать ровно на те же данные.
Рассмотрим выдуманный пример:
1. Некто, назовём его А, пишет утилиту, которая делает две вещи:
— ставит атрибут "compressed" файлам в заданном списке
— подсчитывает сумму размеров тех из них, которые не входят в список игнорируемых.
Делает А это примерно так (на воображаемом языке):

public static int Main(string[] args)
{
  var mask = args.Length > 1 ? args[1] : "*.*";
  List<string> filePaths = GetFilePathsRecursive(mask);  
  SetAllCompressed(filePaths);                              // I
  List<IgnoreMask> ignoredMasks = LoadMasksFromGitIgnore(); 

  filePaths.Remove(f => ignoredMasks.Any(m=>m.IsMatch(f))); // II
  var totalSize = GetTotalSize(filePaths);
  Console.WriteLine($"Total size of ignored files is {totalSize} bytes");
}
  
public static void SetAllCompressed(List<string> filePaths)
{
  for(var i = 0; i < filePaths.Length; i++)
    SetCompressed(filePaths[i]);
}
...


Погоняв тесты, A убеждается, что всё работает, как часы.

2. Через какое-то время некто B отмечает, что работу метода SetAllCompressed можно оптимизировать — ведь включение компрессии и вычисление длины друг от друга никак не зависят. Давайте запускать включение компрессии в другом потоке!
Сделать это очень легко, и его код становится примерно таким:
public static int Main(string[] args)
{
  var mask = args.Length > 1 ? args[1] : "*.*";
  List<string> filePaths = GetFilePathsRecursive(mask);  
  Task compressTask = SetAllCompressed(filePaths);          // I
  List<IgnoreMask> ignoredMasks = LoadMasksFromGitIgnore(); 

  filePaths.Remove(f => ignoredMasks.Any(m=>m.IsMatch(f))); // II
  var totalSize = GetTotalSize(filePaths);
  Console.WriteLine($"Total size of ignored files is {totalSize} bytes");
  compressTask.Wait();
}

public static Task SetAllCompressed(List<string> filePaths)
{
  return new Task(
    () => 
    {
      for(var i = 0; i < filePaths.Length; i++)
        SetCompressed(filePaths[i]);
    }
  );
}


Что может пойти не так? Взяли отлаженный код, чуть поправили — и вперёд.

Однако затем B замечает, что, во-первых, утилита почему-то сжимает не все файлы. А иногда она вылетает с IndexOutOfBounds — но это редко.
Понятно, что проблема — в несоответствии ожиданий. SetAllCompressed ожидает, что переданный ей список будет неизменным в течение всей её работы; а Main грубо нарушает это ожидание в точке II.
Ок, B пытается решить проблему:
public static Task SetAllCompressed(IReadOnlyList<string> filePaths)
{
  return new Task(
    () => 
    {
      for(var i = 0; i < filePaths.Length; i++)
        SetCompressed(filePaths[i]);
    }
  );
}

Теперь, наверное, код Main перестанет компилироваться, и A будет вынужден его исправить.
Отнюдь — ООП работает не так; SetAllCompressed может потребовать изменяемости от своего параметра, но не может потребовать неизменности.
Что остаётся бедному B? Либо городить синхронизацию (и заставляя программу стать обратно однопоточной), либо опять делать защитную копию:
public static Task SetAllCompressed(IReadOnlyList<string> filePaths)
{
  var localPaths = new List<string(filePaths) // выполняем в вызывающем потоке
  return new Task(
    () => 
    {
      for(var i = 0; i < localPaths.Length; i++)
        SetCompressed(localPaths[i]);
    }
  );
}


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

Вот тут мы и приходим к https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutablelist-1
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[3]: Никогда не недооценивайте силу по умолчанию
От: Sinclair Россия https://github.com/evilguest/
Дата: 03.10.22 06:03
Оценка:
Здравствуйте, netch80, Вы писали:
N>На чём будет написан рантайм Go, реализующий шедулер горутин, и почему этот язык не является нормальным?
Ну это же классический вопрос, с не менее классическим ответом.
С точки зрения тьюринг-полноты более-менее все современные языки эквивалентны.
Вопрос как раз в том, как мы ограничиваем возможности программиста выстрелить себе в ногу.
Пока что всё выглядит так, что мы берём небезопасный язык X, и на нём реализуем некоторый искусственно ограниченный язык Y, в котором выстрелить себе в ногу значительно сложнее, чем в X.
Вот, например, java. В ней вызвать разрушение памяти значительно сложнее, чем в любом нативном языке вроде C, C++, или Паскаля.
Зато вот многопоточность в ней — рудиментарная. Написать на ней некорректную многопоточную программу всё ещё значительно проще, чем корректную.
Поэтому прогрессивное человечество ищет нотации, альтернативные навязшим в зубах fork/join и thread.start() / thread.interrupt().
Горутины, Cω, async методы из C# и javascript — всё это попытки решить эту задачу.

Так что не очень важно, на каком языке будет написан рантайм. Он не будет нормальным ровно потому, что в нём вынужденно будет реализована возможность выстрелить себе в ногу, и её предотвращение будет ответственностью программиста, а не компилятора.

Точно так же, как рантайм type-safe java написан на type-unsafe C++. Это не делает С++ type-safe языком.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.