Нелокальные управляющие конструкции считать вредными
От: Tilir Россия http://tilir.livejournal.com
Дата: 22.03.13 09:42
Оценка: 48 (5) +3 -6 :))) :))
Дейкстра в классической статье [1] приводит несколько серьёзных доводов против нелокального goto. В частности, он пишет: "наши интеллектуальные возможности в основном направлены на анализ статических отношений и, напротив, наши способности представлять ход процессов, разворачивающихся во времени, развиты сравнительно слабо. Поэтому мы должны (как умные программисты, осознающие, собственные ограничения) делать всё от нас зависящее, чтобы уменьшать концептуальный зазор между статическим кодом программы и динамическим процессом её выполнения, добиваясь как можно более тривиального соответствия между программой (разворачивающейся в виде текста) и порождённым ей процессом (разворачивающимся во времени)".

Очень похожие слова мы можем встретить в современном документе Google C++ Style Guide [2] но там они касаются уже исключений (C++ exceptions): "исключения делают поток управления программы сложным для оценки при просмотре кода: функции могут вернуть управление в неожиданных местах. Вы можете минимизировать это, соблюдая некие правило относительно того как и где можно использовать исключения, но ценой этого будет то, что разработчик должен будет помнить и знать гораздо больше, чем ему нужно [для работы над его проблемой]". Такую же критику исключений приводит Джоэль Спольски [3] добавляя также "невидимость в исходном коде" и отмечая, что "даже тщательный анализ кода в этих случаях не помогает выявить проблемы".

Очень похожие аргументы о недетерминированном поведении кода приводит Edward A. Lee из Berkeley в своей работе [4], посвященной критике нитей исполнения (threads). Он пишет: "мы должны (и можем) строить детерминированные модели параллельного исполнения, в которые обоснованно и осторожно вносить недетермнизм там, где он реально нужен. Нити исполнения предлагают противоположный подход. Они делают программы абсурдно недетерминистичными и сильно полагаются на хороший стиль программирования, чтобы ограничить этот недетерминизм и достичь детерминированных целей"

Все эти три цитаты говорят об одном и том же -- о принципиальной вредности нелокальных управляющих конструкций. Какие вообще нелокальные управляющие конструкции мы знаем? Очень много, увы. Дейкстра был счастливчиком имея дело всего лишь с GOTO. Вот только некоторые источники головной боли в наше время:

* Нелокальный GOTO
* Continuations
* Исключения
* Сигналы
* Нотификации
* Сопрограммы
* Генераторы
* Нити исполнения
... имя им легион ...

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

Сложность жизненного цикла программы это не только сложность разработки, но и сложность поддержки. Большинство признанных сегодня методов разработки, таких как структурное программирование, ООП и паттерны проектирования, etc направлены на борьбу со сложностью и делают разрабатываемые системы более прозрачными, более соответствующими Principle Of Least Astonishment [5], и в конечном итоге более простыми в поддержке. В противовес этому, нелокальные управляющие конструкции дают прирост скорости разработки за счёт потери детерминированности и простоты разрабатываемого приложения.

Эрик Рэймонд в "Искусстве программирования под Unix" пишет: "Программную систему можно назвать прозрачной, если в ней нет тёмных углов и скрытых глубин. Прозрачность это пассивная характеристика. Программа прозрачна когда вы можете сформировать простую умственную модель её поведения, которая действительно предскажет все или большинство случаев работы с программой, потому что вы можете через всю сложность реализации, тем не менее понимать, что происходит".

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

В функциональных языках нелокальные конструкции, такие как pattern matching или ленивые списки, не вызывают особой боли, потому что являются естественной частью non-modifyable мира, где модификация загнана внутрь монад и интеллектуальные ресурсы разработчика не расходуются на поддержку консистентности модифицируемых структур данных при нелокальных переходах. Но по многим причинам (в частности из-за высокого входного порога и принципиально низкой производительности на существующих архитектурах -- представьте, например, что вместо модификации одного узла дерева, вы полностью уничтожаете и создаёте заново весь миллион его узлов), такие языки до сих пор представляют скорее академический интерес. Что касается нелокальных конструкций в модифицируемом мире, то мы должны отказаться либо от них, либо от оператора присваивания. К тому, же в мире чисто функционального программирования, детерминированности остаётся ещё меньше.

В плане производительности и детерминированности, следует также отметить, что очень часто их использование сопряжено с существенным и невидимым (или сложно оцениваемым) оверхедом. Например Glodstein, Rowe и Kesden из Карнеги-Меллон [6] особо предупреждают о существенном оверхеде при использовании сигналов Unix и переключении контекста процессов.

В отчёте С++ performance technical report [7], про исключения аккуратно сказано: "для некоторых программ проблему составляет сложность в предсказании времени, требующегося для передачи управления от throw до подходящего catch" и предлагается провести для каждой точки возможного выброса исключений сложный анализ, включающий анализ времени работы всех деструкторов объектов, которые могут быть вызваны при раскрутке стека, который позволит установить верхнюю и нижнюю границу времени выполнения. Это очень типичный аргумент для всех адептов нелокального счастья -- они советуют писать код с использованием всех их прекрасных возможностей, а уже потом при отладке многопоточного приложения с исключениями и сопрограммами искать узкие места и думать над тем, откуда это счастье выкинуть. И это явно не инженерный подход.

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

Именно поэтому я предлагаю все нелокальные управляющие конструкции считать вредными.

Литература

1. http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html
2. https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions
3. http://www.joelonsoftware.com/items/2003/10/13.html
4. http://www.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1.html
5. http://c2.com/cgi/wiki?PrincipleOfLeastAstonishment
6. http://www.cs.cmu.edu/~213/lectures/14-signals.pdf
7. http://www.open-std.org/jtc1/sc22/wg21/docs/TR18015.pdf
Re: Нелокальные управляющие конструкции считать вредными
От: D. Mon Великобритания http://thedeemon.livejournal.com
Дата: 24.03.13 15:31
Оценка: :))) :))) :))) :)))
Здравствуйте, Tilir, Вы писали:

"Глаголы — говно. Я презрение глаголы. Злость на людей, которые их похвала."
Re: Нелокальные управляющие конструкции считать вредными
От: maxkar  
Дата: 22.03.13 16:26
Оценка: 67 (9) +2
Здравствуйте, Tilir, Вы писали:

Дейкста и гугл говорят про языки с ручным управлением памятью. Не будем забывать об этом.

T>... в которые обоснованно и осторожно вносить недетермнизм там, где он реально нужен. Нити исполнения предлагают противоположный подход."

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

T>Все эти три цитаты говорят об одном и том же -- о принципиальной вредности нелокальных управляющих конструкций.

Нет. Они говорят о принципиальной вредности нелокальных управляющих конструкций в определенном контексте. В том числе — в контексте проводимых операций, контексте управления ресурсами и т.п. Если сменить контекст, вредность может стать полезностью.

T>* Continuations

Полезны в определенных случаях. Какие-нибудь data flow строить, например. Еще лучше — динамически строить тот же data flow. Вручную перекладывание данных в "процедурном стиле" из одного источника в другой будет ничуть не лучше.

T>* Исключения

Исключительно удобны в "грязном" контексте. Работа с любыми внешними ресурсами. Там же любая операция с ресурсом может вернуть ошибку. И в этом случае на каждой операции нужно вручную(!) проверять код ошибки и при необходимости выполнять очистку ресурсов. Т.е. мы "сильно полагаемся на хороший стиль программирования" в вопросах о том, что ошибка будет замечена вовремя и правильно обработана. Ну и что на что мы поменяли? По мне проще помнить, что у нас грязный контекст, а не писать кучу монотонного ручного кода.

T>* Сопрограммы

T>* Генераторы
Это которые yield return? Так удобно же! Код "генератора вручную" читать и писать гораздо тяжелее, чем yield return. Это, конечно, касается только "чистого" контекста, в котором процедура-генератор не использует ручное управление ресурсами. Т.е., например, читать строки из файла — плохой тон (а вдруг кто-то после первой строки выбросит генератор, не дочитывая до конца и не дожидаясь выполнения закрытия файла).

T>* Нити исполнения

А что взамен? Выше я уже писал, что они используются только там, где по-другому нельзя. Межпроцессное взаимодействие (в сложном виде вроде shared memory и signals) обычно тоже только там, где совсем надо.

T>Каждая нелокальная управляющая конструкция имеет локальный и детерминированный эквивалент.

Да пожалуйста. Приведите аналог "нитей исполнения" в локальном и детерминированном эквиваленте. И желательно в контексте, когда в одном потоке мы читаем файл, а второй обрабатывает события пользовательского интерфейса (хотя бы только перерисовку, ладно уж).

T>Нелокальный GOTO и необходимость использования continuations могут быть ликвидированы при хорошей структурированности программы,

Да. Вопрос только — какой ценой. Вот, пожалуйста, пример. Async.applySync(someFunction, [loadDataA, loadDataB, preloadImages]). Эта страшная штука применяет someFunction после того, как будут загружены данные A, B, и предзагружены картинки. Вся загрузка — асинхронная (сетевая) и параллельная. Это не совсем continuations, (параметров многовато), ну да ладно. Для усложнения ситуации там до этого вполне может быть loadDataB = Async.applyAsync(loadDataB, [loadDataA, loadDataC]), т.е. данные B загружаются после загрузки данных A и C (мало ли что там, ссылки на другие данные, например).

T> исключения и нотификации -- заменены обработкой кодов возврата,

Угу. Обработкой кодов возврата в каждой строчке! Т.е. высокой дисциплиной.
T> сопрограммы и генераторы -- вызовами функций,
В смысле? Вызовами каких функций? Вы предлагаете все, что делает генератор, сначала сложить в память и потом вернуть? А в чем профит? Или как вы "вызовом функций" реализуете ту же банальную конкатенацию списков:
def concat(a, b):
  for elt in a:
    yield return elt;
  for elt in b:
    yield return elt;

Можно, конечно, явным состоянием. Но это будет уже далеко не банальный вызов функции.

T>нити исполнения -- детерминированным параллелизмом на уровне компилятора, таким как OpenMP.

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

T>Тем не менее они возникают снова и снова в разных видах из-за ложного чувства упрощения разработки, которое они дают.

Они дают. Естественно, не везде. Но в своих контектсах — дают. Примеры выше.

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

Да нет, не только разработки. Чтения тоже (yield return читать проще, чем большой и страшный автомат). Кстати, вы от OOP и наследования от интерфейсов тоже предлагаете избавится? Они же недетерминизм вносят (как только мы dependency заинжектили — все, детерминизм кончился).

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

И касается это в первую очередь внешнего API... Не кода, не того, что и куда там переходит. А внешнего API.

T>это часть неявного контракта на него. Безопасность относительно исключений или безопасность относительно многопоточности, которую должен обеспечить программист это такой же нонсенс, как "безопасность относительно глобальных переменных". Последнюю мы понимаем только в одном смысле -- надо выкинуть глобальные переменные.

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

Кстати, как вы хотите бороться с "глобальным состоянием" там, где оно существует исходя из контекста задачи? Банальный почтовый клиент возьмите. Вот пришло пользователю новое сообщение. Это глобальное или локальное состояние? Или нам весь мир нужно перестроить (да, уже там все будет достаточно сложно, потому что событие произошло далеко внизу и придется распутывать сплетения монад друг над другом). А самое интересное, что в процессе "перестроения мира" выяснится, что пользователь подвел курсор мышки к кнопочке и у нас анимация играется уже 0.2 секунды (и осталось еще 0.4 секунды проигрывания). Да и на экране нужно вывести что-нибудь, а это "глобальное состояние" ОС, монитора и пользователя, так что еще и все состояние OS нужно куда-нибудь передать и создать. Казалось бы, простейшая задача. А куда ни плюнь — везде глобальные переменные...

Я не спорю, что от всего "небезопасного" можно отказаться. Но только в достаточно специфичном классе приложений, которые работают практически без взаимодействия со внешним миром.

T>где модификация загнана внутрь монад и интеллектуальные ресурсы разработчика не расходуются на поддержку консистентности модифицируемых структур данных при нелокальных переходах.

Это сказки все про монады. К каждой монаде прилагатеся portable black hole в виде fail :: String -> m A. Это потрясающий недетерминизм в силу своего определения. Плюс он еще и нетипизированный. Если потом где-то откопаешь и выловишь этот fail, еще долго будешь разбираться, что же там произошло на самом деле. Уж лучше типизированные исключения (которые потом и заматчить можно, и все остальное)... Собственно, я поэтому для развлечений все-таки сменил haskell на scala. Мне нужны не "строковые" ошибки, мне нужны ошибки, которые я могу обрабатывать (например, локализовывать потом). Обратите внимание, что нет ни одного parser combinator library, у которой ошибки парсинга строго типизированы (они все строками через fail проползают)! И вряд ли появится в ближайшее время. В силу системы типов. А на исключениях я могу делать все, что нужно. Правда, компилятором они не проверяются (не умеют пока компиляторы нормально обрабатывать union types, нужные для нормальной композции функций с исключениями).

T>В отчёте С++ performance technical report [7], про исключения аккуратно сказано: "...

А оно так часто надо? Тем более, что и без исключений нельзя дать точную границу. Как вы будете оценивать время, занимаемое деструкторами объектов. Да и теми же банальнымы сишными free? (hint — что-нибудь могло засвопиться). Или какого-нибудь close(fd)? Основная проблема не в исключениях ведь. А в том, что там уже где-то образовалась достаточно сложная структура, которую и без исключений сложно детерминированно разобрать. Кстати, на кодах ошибок у вас может такая же структура образоваться со все тем же детерминизмом.

T>Это очень типичный аргумент для всех адептов нелокального счастья -- они советуют писать код с использованием всех их прекрасных возможностей, а уже потом при отладке многопоточного приложения с исключениями и сопрограммами искать узкие места и думать над тем, откуда это счастье выкинуть. И это явно не инженерный подход.

Это явно инженерный подход. Как показала практика, попытка предварительно все учесть в мелочах обычно кончается тем, что оптимизируется далеко не то, что на самом деле нужно. Вот в том, тчо это не математический подход соглашусь. Нет детерминизма, нет каких-то явно доказанных гарантий. Зато оно решает реальные задачи во вполне реальном физическом мире. И вот это то и есть вполне инжеренный подход — где можем, используем математику. Где не можем — не используем. Только математики комплексуют и обижаются иногда

T>При проектировании надёжных, стабильно работающих приложений, мы должны, уже на этапе выбора архитектуры, инструментов и методологий, сводить к минимуму дорогостоящую отладку и поддержку на поздних этапах существования продукта. ... Речь идёт о принципиальном отказе от недетеринизма, так же, как мы отказываемся от нелокальных переходов и убираем зависимости от глобальных переменных.


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

T>Именно поэтому я предлагаю все нелокальные управляющие конструкции считать вредными.

А локальные переменные (которые мутабельные) вы еще не предлагали запретить? Про них можно все те же аргументы привести, что и про глобальные переменные (только чуть в меньшем масштабе). У меня на них (т.е. на чем-то мутабельном) парсер кода (который в мозгу) раза в 3-4 сразу замедляется. Ну за исключением ситуации с fold/map/filter (это когда за переменной сразу специфический цикл идет). А это ведь сложность поддержки... Кстати, код с исключениями (checkd, ибо java) я читаю в среднем гораздо быстрее, чем код на кодах ошибок. Нужные контракты и гарантии обычно проверяются наличием правильного try/finally. На кодах ошибок там бы лесенки были...

P.S. Исключения — это локальный переход! Так же, как и break/continue/return. Типичная монада Either<AbruptCompletion, R>. Прекрасно разворачивается в последовательный код (с проверкой состояния на каждом шаге) и после этого ничем не отличается от возвратов на кодах ошибок.
Re: Нелокальные управляющие конструкции считать вредными
От: Кодт Россия  
Дата: 26.03.13 21:43
Оценка: 37 (3) +2
Здравствуйте, Tilir, Вы писали:

T>Дейкстра в классической статье [1] приводит несколько серьёзных доводов против нелокального goto. В частности, он пишет: "наши интеллектуальные возможности в основном направлены на анализ статических отношений и, напротив, наши способности представлять ход процессов, разворачивающихся во времени, развиты сравнительно слабо. Поэтому мы должны (как умные программисты, осознающие, собственные ограничения) делать всё от нас зависящее, чтобы уменьшать концептуальный зазор между статическим кодом программы и динамическим процессом её выполнения, добиваясь как можно более тривиального соответствия между программой (разворачивающейся в виде текста) и порождённым ей процессом (разворачивающимся во времени)".


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

<>

T>Очень похожие аргументы о недетерминированном поведении кода приводит Edward A. Lee из Berkeley в своей работе [4], посвященной критике нитей исполнения (threads). Он пишет: "мы должны (и можем) строить детерминированные модели параллельного исполнения, в которые обоснованно и осторожно вносить недетермнизм там, где он реально нужен. Нити исполнения предлагают противоположный подход. Они делают программы абсурдно недетерминистичными и сильно полагаются на хороший стиль программирования, чтобы ограничить этот недетерминизм и достичь детерминированных целей"


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

T>Все эти три цитаты говорят об одном и том же -- о принципиальной вредности нелокальных управляющих конструкций.


Не о вредности, а о неподъёмности для средних умов. (А статические анализаторы кода — это посредственные умы, просто очень въедливые).

T> Какие вообще нелокальные управляющие конструкции мы знаем? Очень много, увы. Дейкстра был счастливчиком имея дело всего лишь с GOTO. Вот только некоторые источники головной боли в наше время:


T>* Нелокальный GOTO

T>* Continuations
T>* Исключения
T>* Сигналы
T>* Нотификации
T>* Сопрограммы
T>* Генераторы
T>* Нити исполнения
T>... имя им легион ...

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


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

Разве лишь человеческая лень заставит переосмыслить код и сделать всё то же самое по-другому и проще.

А что касается ООП, так я напомню: у объекта есть поведение и состояние. Говнокод на машине состояний ничем не лучше говнокода на сопрограмме.
Только если мы эту машину состояний родим из какого-то формального описания, которое поддаётся верификации.
Если родим её руками или напишем собственный велосипед, который должен работать как машина состояний — никакой статический анализатор не осилит.

<.....>
T>Именно поэтому я предлагаю все нелокальные управляющие конструкции считать вредными.

и вернуться в однопоточный ДОС.
Перекуём баги на фичи!
Re: Нелокальные управляющие конструкции считать вредными
От: Evgeny.Panasyuk Россия  
Дата: 26.03.13 18:18
Оценка: 6 (1) +3
Здравствуйте, Tilir, Вы писали:

T>Все эти три цитаты говорят об одном и том же -- о принципиальной вредности нелокальных управляющих конструкций. Какие вообще нелокальные управляющие конструкции мы знаем? Очень много, увы. Дейкстра был счастливчиком имея дело всего лишь с GOTO.


http://www.youtube.com/watch?v=c76xD3qNLc4&amp;t=49m26s

Я очень горд, что в этой книжке (Elements of Programming) есть, вы сейчас просто @#$%&%@#, GOTOS. Почему? Потому что в какой-то момент мы показываем, специально, потому что знаем, что вы будете кричать БУУУУУ и писать письма, может быть не все, но некоторые — мне всегда пишут письма что я дурак — потому что я не верю в объектную ориентацию, я использую goto — пропащий человек. И многие люди, в программировании есть такая идея что люди считают что это очень мило написать незнакомому человеку письмо говорящие что "ты дурак". Даже если вы так думаете — вы мне не говорите, потому что — не нужно, не обязательно.
Мы используем goto, для чего — потому что у нас есть структура которая реально является конечным автоматом. Если вы описываете конечный автомат, что он делает — он переходит из состояния этого в состояние то. Как нужно переходить из одного состояния в другое? Есть правильная инструкция — называется goto: идите от туда — туда. И структура программы становится очень элегантной.
То есть идея Дейкстры, Дейкстра великий человек — вы не думайте что $#%@#$%@#$, но он конечно же был не прав, потому что он думал, что плохая программа, потому что она пользуется какой-то синтаксической структурой. Нет — программа плохая, если она плохая.
То есть может быть очень красивая программа на языке Assembler — очень элегантная. Может быть очень плохая программа на Java, уверяю вас.
Немецкий язык такой непоэтичный — но какая поэзия Самые красивые песни на каком языке? На немецком — Шуберт — необыкновенная красота. Хотя у них там гортанный, странный язык, но невероятно красивый.
Всё можно сделать. goto может быть красивым. Я не верю в синтаксические лишения в семантических вопросах.
В меня всю жизнь камнями кидают, потому что я всё время такие вещи говорю — и мне говорят что я дурак. Но я не пытаюсь просто сказать дерзость. Я на самом деле пытаюсь объяснить какие-то вещи и в этой книжке никаких дерзостей.

Re: Нелокальные управляющие конструкции считать вредными
От: Evgeny.Panasyuk Россия  
Дата: 26.03.13 18:48
Оценка: 2 (1) +3
Здравствуйте, Tilir, Вы писали:

T>* Continuations


Иногда полезно использовать — упрощают структуру.

T>* Исключения


Намного чаще приносят пользу чем вред.

T>* Сопрограммы


Иногда полезно использовать — упрощают структуру.
Например при программировании асинхронных потоков — помогают спрятать много синтаксического шума в библиотеку.

T>* Генераторы


Отличный синтаксис для генерации single pass последовательностей — упрощают код.

T>Каждая нелокальная управляющая конструкция имеет локальный и детерминированный эквивалент.

T>Нелокальный GOTO и необходимость использования continuations могут быть ликвидированы при хорошей структурированности программы, исключения и нотификации -- заменены обработкой кодов возврата, сопрограммы и генераторы -- вызовами функций

Каждую программу можно выразить в терминах машины тьюринга.
Каждую программу можно выразить на языке unlabmda в котором всего две встроенные функции(i — не нужен) и один оператор + пару функций для I/O — все на unlambda!

T>В отчёте С++ performance technical report [7], про исключения аккуратно сказано: "для некоторых программ проблему составляет сложность в предсказании времени, требующегося для передачи управления от throw до подходящего catch" и предлагается провести для каждой точки возможного выброса исключений сложный анализ, включающий анализ времени работы всех деструкторов объектов, которые могут быть вызваны при раскрутке стека, который позволит установить верхнюю и нижнюю границу времени выполнения.


"некоторых программ", это:

Enable exception handling for real-time critical programs only if exceptions are actually used. A complete analysis must always include the throwing of an exception, and this analysis will always be implementation dependent.
On the other hand, the requirement to act within a deterministic time might loosen in the case of an exception (e.g. there is no need to handle any more input from a device when a connection has broken down).

А конкретно, для <b><u>JOINT STRIKE FIGHTER AIR VEHICLE</u></b>.

T>Именно поэтому я предлагаю все нелокальные управляющие конструкции считать вредными.


I do not buy the author's arguments either. I keep running into this meme at least a few times every year "exceptions are bad, they're a nightmare, etc..."

My typical reply is also somewhere along the same lines: if you don't like to use exceptions, then just don't use them. Leave that job to the professionals

(c) не мое
Re: Нелокальные управляющие конструкции считать вредными
От: Abyx Россия  
Дата: 22.03.13 11:19
Оценка: +4
Здравствуйте, Tilir, Вы писали:

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

т.е., вот это — не корректная программа (исключения не используются):
uint n = file.get_size();
void* buf = alloc(n);
file.read_to_buf(buf, n);


вот это — корректная программа без исключений, с макросами, как например в исходниках 7zip
#define RNOK(e) if (err_t err = e) return err

uint n;
RNOK(file.get_size(n));
void* buf;
RNOK(malloc(buf, n));
RNOK(file.read_to_buf(buf, n));


но макросы зло, а SEME "сложен для понимания", по этому вот SESE вариант:
err_t e = E_OK;
uint n;
e = file.get_size(n);
if (!e)
{
    void* buf;
    e = malloc(buf, n);
    if (!e)
    {
        e = file.read_to_buf(buf, n));
    }
}
return e;


такой код не имеет скрытых путей выполнения и потому удобен для понимания.




теперь насчет многопоточности.
многопоточность обычно обусловлена требованиями к программе,
по производительности — "пользозователь нажал кнопку — программа отработала за время <0.01 сек",
или по отзывчивости UI — "пользозователь нажал кнопку — UI не должен виснуть".

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

void UI_Controller::validateFile()
{
    string localFileMd5, remoteFileMd5;
    thread th1([&]{ localFileMd5 = md5(fileName); });
    thread th2([&]{ remoteFileMd5 = http_get("http://site/md5/" + fileName); });
    th1.join();
    th2.join();

    ui.invoke([=]{ ui.checkbox.set(localFileMd5 == remoteFileMd5); });
}


никакой OpenMP тут не поможет, зато могут помочь continuations или сопрограммы (в т.ч. async/await).

без исключений тоже тяжело, надо же как-то передавать ошибки между потоками.
In Zen We Trust
Re[17]: Нелокальные управляющие конструкции считать вредными
От: Sinix  
Дата: 28.03.13 10:57
Оценка: 68 (2) +1
Здравствуйте, AlexRK, Вы писали:

ARK>Нечто типа "lazy result.Add(item)".

ARK>yield return — это скачок управления в вызывающий код.

Т.е. lazy result.Add() — это не скачок? Ок, пишем свой awaiter — обёртку вокруг IEnumerator<T> и заменяем yield return на await enumerator.Yield(item). Что принципиально поменялось?

S>>То, что вы предлагаете, либо убого по сравнению с yield return по функциональности, либо предлагает ровно то же. То есть в вашем коде нет абсолютно никаких преимуществ перед yield return.

ARK>Преимущество в одном выходе из метода. Собственно, с чего и начался весь разговор.
Не поверите, в итераторах тоже ровно один способ явно выйти — yield break. Замените "yield return" на "yield next", "yield break" на "yield exit" — получите желаемый синтаксис

ARK>Потенциальная оптимизация в том, что контейнер-результат не надо хранить целиком.

Даже при однократном переборе автоматическая оптимизация может стоить _очень_ дорого. В практике был случай, когда отказ от итератора и предварительное заполнение того самого контейнера ускоряло код (числомолотилка) в ~2-5 раз в зависимости от объёма кэша процессора. Если делать оптимизацию руками — никакой принципиальной разницы между yield return и lazy не будет.

ARK>По моему мнению, чтобы эффективно писать программы с действительно массовым параллеллизмом (тысячи потоков), многопоточный код должен всячески мимикрировать под однопоточный. А у вас наоборот получается. Все ИМХО.


У вас тут путаница между параллельными вычислениями и многопоточным (оно же multicore, оно же распределённое) программированием.
Идее "допилим императивный язык для multicore-вычислений" фиг знает сколько лет и она всё никак не взлетит. Для большинства прикладных задач за глаза хватает "распараллель мне вот этот цикл" и "Запусти вот этот код и продолжи по завершению". В шарпе, соответственно — PLinq и Task+await.

Для более-менее серьёзного multicore придётся отказаться от разделяемого состояния и перейти к передаче сообщений и акторам (угу, CAP theorem, только в профиль). На классических императивных ЯП такие вещи выглядят крайне многословно и коряво. Проще вытащить код, который _реально_ выиграет от распределённых вычислений в отдельную подсистему на скале/эрланге.
Re[7]: Нелокальные управляющие конструкции считать вредными
От: SergH Россия  
Дата: 22.03.13 11:38
Оценка: 7 (1) +1 :)
Здравствуйте, Erop, Вы писали:

E>Часто всё-таки есть конфликт интересов между "всем удобнее и проще", и "выгоднее для бизнеса"...

E>Поэтому в подобных случаях ЭКОНОМИЧЕСКОЕ и ТЕХНИЧЕСКОЕ обоснования таки требуются, а по умолчанию, без обонований -- запретить.

Мне сложно представить себе экономическое обоснование на использование/запрет чего-либо. Это что-то типа

... как показывают исследование, А% программистов С++ с опытом от года понимают эту конструкцию правильно, так что если все теперешние программисты уволятся и придется набирать новых, проблемы с использованием этого возникнут с вероятностью Б%. Если повысить требования к набираемым программистам, проблемы возникнут с вероятностью В%, но при этом зарплаты придется повысить на Г% и риск не найти на рынке специалистов повысится до Д%. Уровень проблем оценивается как Ж-12-47.

При этом выгоды от использования данной конструкции составляют Е% уменьшения объёма кода и Ё% повышение его читабельности (для тех, кто понимает смысл конструкции правильно).

Оценки лежат в доверительном интервале +/- 2% с вероятностью 95%. Мы считаем выгоды значительными а риск приемлемым и рекомендуем разрешить.


Хотелось бы посмотреть на такие исследования.
Делай что должно, и будь что будет
Re[2]: Нелокальные управляющие конструкции считать вредными
От: denis8158  
Дата: 22.03.13 14:43
Оценка: 1 (1) +1
Здравствуйте, Abyx, Вы писали:

A>Здравствуйте, Tilir, Вы писали:



A>но макросы зло, а SEME "сложен для понимания", по этому вот SESE вариант:

A>
A>err_t e = E_OK;
A>uint n;
A>e = file.get_size(n);
A>if (!e)
A>{
A>    void* buf;
A>    e = malloc(buf, n);
A>    if (!e)
A>    {
A>        e = file.read_to_buf(buf, n));
A>    }
A>}
A>return e;
A>


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

Но и это ещё не все, если мы этот код используем в достаточно сложной бизнес логике, которая и так сложная (сотни условий, разные циклы для подготовки данных, для расчетов и т. п. ). Так теперь ещё и после вызова каждой функции нам требуется проверять код её ошибки??? Даже в этом примере нам понадобилось два уровня if'ов. А уже в бизнес логике потребуются сотни и всё для того, чтобы не выполнить код, после возникновения ошибки. Мы тем самым вручную реализуем логику исключений и развертывания стека.

Типун Вам на язык, чтобы такое было в моём благоверном C#....
Re: Нелокальные управляющие конструкции считать вредными
От: VladD2 Российская Империя www.nemerle.org
Дата: 23.03.13 13:03
Оценка: +2
Здравствуйте, Tilir, Вы писали:

T> Но по многим причинам (в частности из-за высокого входного порога и принципиально низкой производительности на существующих архитектурах -- представьте, например, что вместо модификации одного узла дерева, вы полностью уничтожаете и создаёте заново весь миллион его узлов),


Прежде чем судить о мирах тебе не известных ты бы потрудился разобраться в их устройстве. Слова про изменение миллионов узлов — это бред. В неизменяемом мире нет нужды пересоздвать все узлы дерева, если изменился один из них. При этом преподаются только узлы от текущего и до корня. А это обычно очень небольшое число узлов.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re: Нелокальные управляющие конструкции считать вредными
От: k.o. Россия  
Дата: 27.03.13 09:21
Оценка: +2
Здравствуйте, Tilir, Вы писали:

T>Каждая нелокальная управляющая конструкция имеет локальный и детерминированный эквивалент. Нелокальный GOTO и необходимость использования continuations могут быть ликвидированы при хорошей структурированности программы,


Тут было бы интересно увидеть пример ликвидации continuations при помощи хорошей структурированности. Это ведь не голословные утверждения, правда?

T>исключения и нотификации -- заменены обработкой кодов возврата, сопрограммы и генераторы -- вызовами функций, нити исполнения -- детерминированным параллелизмом на уровне компилятора, таким как OpenMP. Тем не менее они возникают снова и снова в разных видах из-за ложного чувства упрощения разработки, которое они дают.


OpenMP для параллелизма это, конечно, здорово, а что ты посоветуешь для concurrency?

T>Для программ со свободными функциями или с методами классов (а это абсолютное большинство работающего сегодня ПО), хорошим стилем является предпочитать явный контракт на метод неявному. Так, например, метод, модифицирующий одну глобальную переменную и сильно зависящий от состояния другой будет иметь два вхождения в своём неявном контракте. Но любая нелокальная конструкция в методе это часть неявного контракта на него. Безопасность относительно исключений или безопасность относительно многопоточности, которую должен обеспечить программист это такой же нонсенс, как "безопасность относительно глобальных переменных". Последнюю мы понимаем только в одном смысле -- надо выкинуть глобальные переменные.


Обеспечение безопасности относительно исключений, принципиально, ничем не отличается от действий требуемых при использовании, например, кодов возврата. Вся шумиха вокруг него связана лишь с тем, что место выброса исключения менее заметно по сравнению с явным использованием 'return' или его аналогов.

Что касается "безопасности относительно многопоточности", то, для начала, стоит привести определение данного термина.

T>В функциональных языках нелокальные конструкции, такие как pattern matching или ленивые списки, не вызывают особой боли, потому что являются естественной частью non-modifyable мира, где модификация загнана внутрь монад и интеллектуальные ресурсы разработчика не расходуются на поддержку консистентности модифицируемых структур данных при нелокальных переходах. Но по многим причинам (в частности из-за высокого входного порога и принципиально низкой производительности на существующих архитектурах -- представьте, например, что вместо модификации одного узла дерева, вы полностью уничтожаете и создаёте заново весь миллион его узлов),


Слабое знание матчасти не освобождает от ответственности за несение бреда.

T>В плане производительности и детерминированности, следует также отметить, что очень часто их использование сопряжено с существенным и невидимым (или сложно оцениваемым) оверхедом. Например Glodstein, Rowe и Kesden из Карнеги-Меллон [6] особо предупреждают о существенном оверхеде при использовании сигналов Unix и переключении контекста процессов.


Ты знаешь как избежать переключения контекста? Хотелось бы подробностей.

T>В отчёте С++ performance technical report [7], про исключения аккуратно сказано: "для некоторых программ проблему составляет сложность в предсказании времени, требующегося для передачи управления от throw до подходящего catch" и предлагается провести для каждой точки возможного выброса исключений сложный анализ, включающий анализ времени работы всех деструкторов объектов, которые могут быть вызваны при раскрутке стека, который позволит установить верхнюю и нижнюю границу времени выполнения. Это очень типичный аргумент для всех адептов нелокального счастья -- они советуют писать код с использованием всех их прекрасных возможностей, а уже потом при отладке многопоточного приложения с исключениями и сопрограммами искать узкие места и думать над тем, откуда это счастье выкинуть. И это явно не инженерный подход.


Иными словами, предлагается провести для каждой точки возможного выброса исключений сложный анализ, включающий анализ времени освобождения всех ресурсов выделенных на момент выброса исключения, который позволит установить верхнюю и нижнюю границу времени выполнения. Имеешь-ли ты ввиду, что при отказе от исключений время выполнения может быть оценено без подобного анализа или же, что он становится существенно проще? Тут опять таки, хотелось бы увидеть альтернативный подход с аргументированным обоснованием преимуществ.
Re[13]: Нелокальные управляющие конструкции считать вредными
От: Sinclair Россия https://github.com/evilguest/
Дата: 27.03.13 11:06
Оценка: +2
Здравствуйте, AlexRK, Вы писали:

ARK>Здравствуйте, FR, Вы писали:


ARK>>>Дык, мы же с этого начали — я сделал предположение, что с сферическим умным компилятором усложнения быть не должно (просто взять LazyList вместо List).


FR>>Не вижу как можно сделать такой компилятор.


ARK>Элементарно же. Вот это

ARK>
ARK>IEnumerable<T> takewhile<T>(Predicate<T> predicate, IEnumerable<T> iterable)
ARK>{
ARK>  var result = new LazyList<T>();

ARK>  foreach (var x in iterable)
ARK>  {
ARK>    if (predicate(x))
ARK>      result.Add(x);
ARK>    else
ARK>      break;
ARK>  }

ARK>  return result;
ARK>}
ARK>

ARK>автоматом переписывается в

ARK>
ARK>IEnumerable<T> takewhile<T>(Predicate<T> predicate, IEnumerable<T> iterable)
ARK>{
ARK>  foreach (var x in iterable)
ARK>  {
ARK>    if (predicate(x))
ARK>      yield return x;
ARK>    else
ARK>      break;
ARK>  }
ARK>}
ARK>


ARK>

ARK>Может показаться, что это попахивает жульничеством. Но нет. Концептуально в первом варианте мы имеем один вход и один выход. А что там внизу — пофиг. В ассемблере вообще одни goto label, и что с того?
Имхо, отвратительный подход.
Во-первых, совершенно неочевидный для программиста. Ленивость — штука крайне важная. В частности, момент, когда вылетит NRE в случае передачи null в качестве аргумента, радикально зависит от ленивости либо энергичности.
Во-вторых, вы взяли изолированный пример, который вам показался удобным. Отлично. Что будет делать ваш компилятор вот в этом примитивном случае:
IEnumerable<T> split(bool takeOdds, IEnumerable<T> iterable)
{
  var odds = new LazyList<T>();
  var events = new LazyList<T>(); 
  int i = 0;
  foreach(var item in iterable)
    if (i++ % 2 == 1)
      odds.Add(item);
    else
      evens.Add(item);

  return takeOdds? odds : evens;
}

Один вход, один выход, все дела.

ARK>Более того — похоже, можно вообще любой метод помечать атрибутом [Lazy] и все! Безо всяких дополнительных управляющих конструкций и без потери читабельности.

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

ARK>А если пойти еще дальше — то ленивость можно сделать полностью автоматической оптимизацией. Я даже думаю, что программист далеко не всегда может сам определить, должен ли метод быть ленивым.


ARK>Где нужна вообще ленивость? (На полноту и истину не претендую.)

ARK>1) Бесконечные структуры данных. Оставим этот вариант безумным ученым. Если кому очень надо, можно сделать вручную.
ARK>2) Отложенные вычисления — надеемся, что (вдруг) вычислять вообще не придется. Это плюс. Минус — это ухудшает реактивность и предсказуемость работы системы в целом. Поэтому опять в топку, при сильной необходимости делаем вручную.
Ну то есть вы предлагаете просто вернуться в С# 1.0. Там ленивость была именно такая — "при сильной необходимости делаем вручную". Не помните, почему мы оттуда ушли?

ARK>Одним словом, мне кажется, что ленивость и yield — это просто высокоуровневая оптимизация.


ARK>Еще раз основная мысль поста: на мой взгляд можно обойтись без yield return и не потерять в читабельности.

Смотря где. В императивных языках с энергичной семантикой — нет, нельзя. Если строить язык с нуля — то скорее имело бы смысл отказаться от коллекций, в которые в принципе можно делать add. Например, заменить их каналами. То есть любой add — это "yield return", который отдаёт управление тому, кто сейчас висит на get().
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re: Нелокальные управляющие конструкции считать вредными
От: minorlogic Украина  
Дата: 27.03.13 12:46
Оценка: +2
Рассуждения по аналогии могут работать а могут не работать.

Наверное вы забыли что исключения это специально спроектированная конструкция призваная решать совершенно определенные задачи. И она их решает.

Пока вы не предложите лУчшее решение, посылать на свалку исключения несколько преждевременно
... << RSDN@Home 1.2.0 alpha 5 rev. 1539>>
Ищу работу, 3D, SLAM, computer graphics/vision.
Re[2]: Нелокальные управляющие конструкции считать вредными
От: LaptevVV Россия  
Дата: 22.03.13 16:33
Оценка: 8 (1)
Здравствуйте, Ikemefula, Вы писали:

I>Здравствуйте, Tilir, Вы писали:


T>>Сложность жизненного цикла программы это не только сложность разработки, но и сложность поддержки. Большинство признанных сегодня методов разработки, таких как структурное программирование, ООП и паттерны проектирования, etc направлены на борьбу со сложностью и делают разрабатываемые системы более прозрачными, более соответствующими Principle Of Least Astonishment [5], и в конечном итоге более простыми в поддержке. В противовес этому, нелокальные управляющие конструкции дают прирост скорости разработки за счёт потери детерминированности и простоты разрабатываемого приложения.


I>Не совсем понятно, что такое нелокальные конструкции.

Это — нелокальные переходы.
Хочешь быть счастливым — будь им!
Без булдырабыз!!!
Re[15]: Нелокальные управляющие конструкции считать вредными
От: Sinclair Россия https://github.com/evilguest/
Дата: 28.03.13 04:33
Оценка: 6 (1)
Здравствуйте, AlexRK, Вы писали:

ARK>Для автора метода все очевидно. А для пользователя разницы нет (это и сейчас так).

Вот как раз для пользователя разница принципиальна. Это и сейчас так.
Банально потому, что его попытка сделать catch(NullReferenceException) внезапно не удастся.

ARK>Ошибка компиляции. LazyList должен быть только один. Но в принципе я согласен, что всякие подводные камни могут быть, возможно нужны и другие ограничения.

Ограничений будет вагон и маленькая тележка. Скажем, как будет себя вести ваш LazyList, если его передать аргументом в энергичный метод?

ARK>Чего ради? Убрать goto-подобные скачки. Считаете это глупым непрактичным пуризмом? Ваше право. Я считаю иначе.

В yield return нет никаких "скачков". То, что вы предлагаете, либо убого по сравнению с yield return по функциональности, либо предлагает ровно то же. То есть в вашем коде нет абсолютно никаких преимуществ перед yield return.

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

Отож. Вы слишком мало читаете Липперта.

ARK>Не совсем просто, с дополнительной оптимизацией в тех местах, где обработка идет поэлементно.

Не вижу никакого простора для дополнительных оптимизаций.
ARK>И, кстати, я не считаю все эволюционные решения C# правильными.
Это я уже понял. Пока что вы ничего интересного взамен не предлагаете.

ARK>Почему "накопление" результата в ленивом контейнере менее читабельно, чем yield return?

Потому, что непонятна семантика такого "накопления".

S>>Если строить язык с нуля — то скорее имело бы смысл отказаться от коллекций, в которые в принципе можно делать add. Например, заменить их каналами. То есть любой add — это "yield return", который отдаёт управление тому, кто сейчас висит на get().


ARK>Можете короткий псевдокод привести? Звучит интересно, но как-то понимание ускользает.

Очень просто всё.
public void IterateTree<T>(Tree<T> tree, Channel<T> channel)
{
channel.push(tree.Value); // происходит переключение контекста, управление передаётся тому, кто сделал channel.pull
foreach(child in tree.Children)
{
IterateTree(child, channel); // рекурсивные вызовы имеют ту же семантику
}
}
Если нет никого, кто бы висел на channel.pull, то всё блокируется до тех пор, пока кто-то не придёт сделать этот запрос.
Отличается от yield return тем, что
— в одном методе можно иметь сколько угодно каналов, а не только единственный результат
— на одном канале могут висеть сколько угодно продьюсеров и консьюмеров. То есть, можно иметь один кусок кода, который принимает письма для отправки, и N "потоков"-потребителей, которые занимаются доставкой писем. Условно, имеем "императивный" код типа
public void handleIncomingMail(Channel<string> smtpSession, Channel<MailMessage> messageQueue)
{
  handleHandshake(smtpSession); // EHLO, login sequence
  while(smtpSession.IsActive)
    handleSingleMessage(smtpSession, messageQueue);
}
public void handleSingleMessage(Channel<string> smtpSession, Channel<MailMessage> messageQueue)
{
  message = new MailMessage();
  message.From = parseMailFrom(smtpSession.pull());
  message.To = parseRcptTo(smtpSession.pull());
  message.Body = parseData(smtpSession.pull());
  messageQueue.push(message);
}
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[19]: Нелокальные управляющие конструкции считать вредными
От: Sinix  
Дата: 28.03.13 13:49
Оценка: 4 (1)
Здравствуйте, AlexRK, Вы писали:

S>>Т.е. lazy result.Add() — это не скачок? Ок, пишем свой awaiter — обёртку вокруг IEnumerator<T> и заменяем yield return на await enumerator.Yield(item). Что принципиально поменялось?

ARK>А что принципиально поменяется, если if заменить на goto label?
Замените

yield, lazy result.Add() и await enumerator.Yield(item) не различаются поведением — они возвращают некоторое значение и возобновляют выполнение метода для получения следующего значения. Семантика не меняется, разница только в синтаксисе. С if и goto такая замена не пройдёт.

S>>Не поверите, в итераторах тоже ровно один способ явно выйти — yield break. Замените "yield return" на "yield next", "yield break" на "yield exit" — получите желаемый синтаксис


ARK>Это уже второй способ. Первый — выход из метода в конце.

ARK>yield next — ну, может, вы и правы. Правда, нельзя посмотреть, например, все точки ленивого выхода, каким-нибудь find usage.

Не поверите, с lazy result.Add всё точно так же — или return в середине, или return в конце метода.
Напомню, yield next — это просто переименованный yield return. Получается, семантика (поведение) не поменялось и проблема только в синтаксисе шарпа и отсутствии в студии пункта меню "найти все yield return в методе"

S>>Даже при однократном переборе автоматическая оптимизация может стоить _очень_ дорого. В практике был случай, когда отказ от итератора и предварительное заполнение того самого контейнера ускоряло код (числомолотилка) в ~2-5 раз в зависимости от объёма кэша процессора. Если делать оптимизацию руками — никакой принципиальной разницы между yield return и lazy не будет.


ARK>Да? Может быть, спорить не буду. Тогда, получается, ленивость не нужна, недаром же эрланг энергичный.


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

В первом случае последовательность "Лениво считаем сумму, сумма запускает ленивое получение записей, получение записей лениво открывает файл и лениво читает строчку за строчкой" ничего не даст — сама последовательность действий императивна, так что всё, чего мы добьёмся — добавим задержки на оповещение о завершении ленивого вычисления. Проще запустить всю цепочку в фоновом потоке и не дёргаться.

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

Но это возможно только если код будет написан подходящим образом — не будет иметь разделяемого состояния и не будет полагаться на определённый порядок выполнения инструкций (например, что текущая позиция stream будет соответствовать текущей обрабатываемой записи). Чтобы писать в таком стиле в классических-императивных языках нужно вводить или ещё один слой абстракции — те самые yield/await, или тщательно следить за тем, как пишешь код. Даже самое маленькое послабление может легко всё поломать. Например, добавили переупорядочивание store-load инструкций в итаниумах — уже никуда без volatile/блокировок/interlocked/понимания что происходит на самом деле *последний пункт — самый важный


S>>Идее "допилим императивный язык для multicore-вычислений" фиг знает сколько лет и она всё никак не взлетит.

ARK>Я не совсем про это. Я скорее про много однопоточных приложений, каждое выполняется на своем ядре. (Ну это мечты, конечно.)

Тоже не взлетит. (Источник и точные цифры не вспомню, но тут важно соотношение), 92% времени типового UI приложения — это ожидание ввода пользователя. Остальные 8% — дикое тормозилово из-за работы в UI-потоке. В случае с Metro UI время работы в UI-потоке уменьшается кардинально за счёт вытаскивания тормозов в await DoSomething(). Общее время выполнения при этом растёт (за счёт накладных расходов), зато отзывчивость UI практически идеальна.

Если мы забудем про UI и перейдём к собственно тяжёлой логике, то там 1 ядро на поток нам не особо поможет — ядра-то слабенькие, а императивный код распараллеливается плохо. Будем распараллеливать всё что можно — наткнёмся на закон Амдала.

Короче: для тяжёлых задач выбор практически не поменялся — или openMP и низкоуровневый хардкор на шейдерах, или специализированные скала с эрлангом, или умные балансировщики и куча мощных машин.

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

Если сможете придумать, накуда пользователю 1000 ядер — велкам. Только плиз, давайте без распознавания объектов, дополненной реальности и анализа генокода по фотографии Как показывает практика, подобные вещи рано или поздно выносятся в узкоспециализированные сопроцессоры, а собственно логика приложения как работала так и работает в одном потоке cpu.
Re[25]: Нелокальные управляющие конструкции считать вредными
От: Sinix  
Дата: 29.03.13 12:56
Оценка: 4 (1)
Здравствуйте, AlexRK, Вы писали:

ARK>Здравствуйте, Sinix, Вы писали:


S>>>>Достаточно обрабатывать события асинхронно — например, вытащить обработку в фоновый поток, что не представляет особых проблем ещё с первого фреймворка.

ARK>>>А это что, не распараллелливание?
S>>Без обид, но у вас все баззворды собраны в одну кучу

ARK>Асинхронность по определению означает наличие второго потока исполнения (реального или виртуального — неважно).

Да ладно Реализации на локальной очереди, через петлю обработки сообщений, через подписку на IObservable, через таски, IO completition ports etc обходятся ровно одной абстракцией — continuation-passing. Зачем здесь вводить понятие "потока исполнения"?

Особенно если учитывать что всё вышеперечисленное протаскивает за собой ExecutionContext, т.е. с логической точки зрения поток исполнения остаётся один.

ARK>В принципе да, но одно всегда влечет другое.

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

Аналогично, Parallel.For может развернуть цикл в несколько потоков, динамически раскидывать по потокам пачки элементов из исходной последовательности, запулить всё в один поток или замусорить очередь таскпула. С концептуальной точки зрения по прежнему ничего не поменяется — мы отдаём управление циклом на откуп рантайму в обмен на возможность загрузить все ядра CPU.

S>>Ещё можно глянуть вот эти статьи — первое найденное, подробно не копался.

ARK>Спасибо, гляну из дома. Хотя и так ясно, что там даже близко нет того, о чем я говорю.
Конечно нет, там всё на банальном моделировании системы частиц. В голове крутится, что кто-то где-то пытался пойти дальше и скрестить модель акторов, конечные автоматы и обработку матриц на GPU, но сейчас хоть убей деталей не вспомню

ARK>Топик про управляющие конструкции в языках программирования. Не про мейнстрим-языки и не про вещи, реализуемые на типовых пользовательских компьютерах.

Ок, тогда встречный вопрос:
кому интересны отдельные конструкции, которые малоприменимы в мейнстриме? Повторюсь, для таких вещей лучше взять специализированный язык с подходящей моделью вычислений и не пытаться впихнуть всё в несчастный шарп
Re: Нелокальные управляющие конструкции считать вредными
От: SergH Россия  
Дата: 22.03.13 09:58
Оценка: -1
Здравствуйте, Tilir, Вы писали:

T>Именно поэтому я предлагаю все нелокальные управляющие конструкции считать вредными.


Вот есть такая полезная штука как логирование.

Довольно удобно иметь возможность её включать, выключать и менять уровень из одного места. Через конфиг или параметры командной строки... Обычно это реализуется с использованием глобальных переменных/синглетонов или чего-то подобного.

Как ты предлагаешь сделать это иначе?
Делай что должно, и будь что будет
Re: Всё зависит от целей разработки. Может кто-то просто учится за счёт работада
От: Erop Россия  
Дата: 22.03.13 09:58
Оценка: :)
Здравствуйте, Tilir, Вы писали:

T>Именно поэтому я предлагаю все нелокальные управляющие конструкции считать вредными.



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

так что их может и надо использовать, но ограниченно и обоснованно, в рамках каких-то точно оптсанных и довольно ограниченных подходов, которые экономически и технически обоснованны.

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

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

То есть, вопрос "как это повысит доходы фирмы" имеет то, волшебное свойстово, что позволяет легко обуздать страсть программистов к красоте и эесперементам со стилем разумными прагматическими рамками.
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re: Нелокальные управляющие конструкции считать вредными
От: VladD2 Российская Империя www.nemerle.org
Дата: 23.03.13 12:54
Оценка: +1
Здравствуйте, Tilir, Вы писали:

T>Какие вообще нелокальные управляющие конструкции мы знаем? ... Вот только некоторые источники головной боли в наше время:


T>* Нелокальный GOTO

T>* Continuations
T>* Исключения
T>* Сигналы
T>* Нотификации
T>* Сопрограммы
T>* Генераторы
T>* Нити исполнения
T>... имя им легион ...

Нелокальный GOTO, исключения и потоки несомненно создают проблемы. Остальное перечисленное притянуто за уши. Бремя доказательства оставляю за тобой как за автором высказывания. Обоснуй, плиз, вред создаваемый хотя бы сопрограммами/генераторами.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[4]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 24.03.13 19:55
Оценка: :)
Здравствуйте, maxkar, Вы писали:

M>А как там interpocess communication реализуется? Мне в идеале хочется гонять структуры между различными частями программы (теми самыми легковесными процессами). Иногда — достаточно большие объемы.


Ну статью-то почитайте. Взаимодействие — через сообщения с семантикой копирования. Для обмена большими кусками данных есть Exchange Heap, но доступ туда — только через уникальные указатели (т.е. в один момент времени — один владелец).

M>Ну вот банальщину возьмем. Игру какую-нибудь сетевую. Один поток ведет сетевой обмен. Второй поток обсчитывает механику (очевидно, должен получать сообщения от сети в удобоваримом виде). Третий поток картинку генерирует к физике. И системный процесс все это безобразие на экран выводит (double buffering в отрисовке). В зависимости от удобства API мы получим либо те же shared memory со всеми ее проблемами (buffered image передавать надо как-то), либо кучу неудобств на ровном месте. Ну или дайте ссылку на конкретный API, я посмотрю. Если что, "высокоуровневые" примитивы обмена ведь и с обычными потоками можно использовать.


Shared memory не обязательно должен иметь типичные проблемы с блокировками, если доступ идет через уникальные ссылки (http://en.wikipedia.org/wiki/Uniqueness_type). Примеры API — ну если только исходники той же Singularity или JX (http://www4.cs.fau.de/Projects/JX/index.html), там доступ к shared data идет через "порталы". Но сам я эти API глубоко не копал, вероятно там есть свои заморочки. Но вряд ли сопоставимые с геморроем с синхронизациями.

ARK>>Писать может и удобно, а вот читать — не уверен.

M>Удобнее, чем явный автомат. Как минимум в типичных случаях. Можно, конечно, и ужасы написать. Но от misuse никто не застрахован.

Фиг знает. Не нравятся мне разбросанные везде точки выхода.

M>Ок. Хорошо. Мы можем в некоторых местах перейти на каналы. Если эффективность нас не особо волнует, наверное, мы много чего сможем на них сделать. В голову вроде бы ничего не приходит невозможного. Остается вопрос эффективности.


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

M>Например, у нас 10 игроков. Мы генерируем уникальные идентификаторы каких-то сообщений (того же чата, например, только не спрашивайте, зачем мы это делаем...). Я правильно понимаю, что без отсутствия shared state мы у специального процесса будем запрашивать генерацию и ожидать ответ на канале?


Ээ... ну наверное да. А как иначе-то. В принципе, получается та же блокировка, но реализованная на системном уровне.

M>>>Async.applySync(someFunction, [loadDataA, loadDataB, preloadImages]).

ARK>>Эта "страшная штука" — опять те же нити. Причем тут continuation? Дайте пример без async.
M>Вот в данном то конкретном случае нитей там и не было Это полностью однопоточный код (и вообще там в рантайме поддержки потоков нет!). Где-то на верхнем уровне он вырождатеся в тот самый цикл на сообщениях:

Э, нет. Если есть "цикл на сообщениях", это получаются те же самые нити, но реализованные вручную. Велосипед с квадратными колесами. А если код истинно однопоточный, то вместо Async.applySync(someFunction, [loadDataA, loadDataB, preloadImages]) мы имеем право с чистой совестью написать:
  loadDataA();
  loadDataB();
  preloadImages();
  someFunction();


M>Да в общем, это и не важно. Как бы вы подобный код написали в однопоточном виде? Ну пусть через те же каналы. В некоторых случаях — через waitForMultipleObjects. Только не везде можно было бы обойтись этим (все равно и каналы новые нужно создавать, и запросы отправлять). Да, я писал вначале "ручные" автоматы (if loadState == ... и if loadedCount == ...). Когда переписал на continuations + async, вздохнул с огромным облегчением.


Ну как, вот так как-то (псевдокод):
  // parent process
  this.data = receive childChannel.GetData();
  someFunction();

  // ...

  // child channel
  public Data GetData()
  {
    loadDataA();
    loadDataB();
    preloadImages();
  }


M>Типичный метод:

M>
M>public function doSomething(app, arg1, arg2) : OperationState<Void> {
M>  var dataA = loadSomethingVeryAsync(arg1);
M>  var dataB = loadSomethingOverAsync(arg2);
M>  var preloadedImages = Async.applyAsync(preloadImagesForData, [dataA]);
M>  var externalData = Async.applyAsync(loadDataFromExternalSystem, [dataB, arg2]);
M>  var appUI = Async.applySync(createUI, [app, dataA,  extenralData, preloadedImages]);
M>  return Async.applySync(app.addDialog, [appUI]);
M>}
M>

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

Тут слишком много асинхронности для обычного метода. Это скорее какой-то диспетчер совсем верхнего уровня, каких в системе единицы.

M>И еще. Обратите внимание на уровень декларативности кода. Ему совершенно без разницы, на потоках происходит вся асинхронность или "в цикле на событиях"! Еще оно прозрачно и ошибки возвращает (ну правда, не вручную же на каждом канале проверять, успешно все закончилось или нет). А все — на continuation (потому что addDialog, createUI, loadDataFromExternalSystem — это некое continuation в самом широком смысле слова).


Вообще, больше обратил внимание на отсутствие типов данных и в связи с этим многое не понял. Что такое dataA, dataB — данные или методы?
Насчет continuation. Здесь разве есть continuation вообще? Continuation — это когда мы вызываем метод и больше не возвращаемся. А у вас обычные асинхронные вызовы.

M>Отсутствие deadlock — это хорошо (без иронии). Может ли singularity гарантировать, что обработчик сообщений справится со всем потоком (т.е. очередь не будет расти бесконечно)? Вопрос серьезный. Например, для "автоматического парралелизма в стиле MPI" это чисто теоретически можно доказать. А иначе опять руками все доказывать...


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

ARK>>Не спора ради, а истины для. Не приведете пример убедительного превосходства yield return?

M>Ну что-то вроде:
M>
M>def listInTree(curNode, pred):
M>  if pred(curNode):
M>    yield return curNode
M>  else:
M>    for sub in curNode.children():
M>      for res in listInTree(sub, pred)
M>         yield return res
M>

M>Можно еще всякие графы более сложные обходить, например. Но это так, навскидку. Я на языки с yield return совсем недавно освоил, поэтому ничего сильно лучше под рукой нет.

  List<TreeNode> ListInTree(TreeNode node, Predicate pred)
  {
    var result = new List<TreeNode>();

    if (pred(node))
    {
      result.Add(node);

      foreach (var childNode in node.Children)
      {
        result.AddRange(ListInTree(childNode, pred));
      }
    }

    return result;
  }

По-моему, читабельность тут не особо ниже (если вообще ниже).

M>>>Кстати, вы от OOP и наследования от интерфейсов тоже предлагаете избавится? Они же недетерминизм вносят (как только мы dependency заинжектили — все, детерминизм кончился).

ARK>>Не понял, какой здесь вносится недетерминизм?
M>Ну как... В вашем же определении "Нет, не локальный, т.к. мы не знаем конечной точки." А конечной точки мы не знаем

А, в таком смысле. Тогда да. Но такой недетерминизм неизбежен, если есть полиморфизм в коде (любой).

M>Очень хорошо. А кто в этих случаях аксиомы доказывает? Пользователь или компилятор? Но даже это не важно. Вот объясните мне, почему мы можем проверить "контракты соблюдены", а "метод выбрасывает только исключения из списка ..." не можем? А если можем проверить "список исключений", то какие тогда вообще проблемы? Исключение — это не переход. Исключение — это результат работы метода! Монада такая. Все, после этого мы все, что угодно, можем доказывать.


Да, checked exceptions — доказуемы. Но вот беда, с ними возни столько, что польза становится сомнительной. Смысла в них особого нет, ИМХО. Сильно прозрачнее код они не сделают. Я это все к тому, что такие исключения — это и не исключения в общем-то (в том смысле, в каком они упоминаются в стартовом посте).

M>Это вряд ли. Переменная в замыкании примерно такая же глобальная переменная, как и все остальные. То, о чем говорил ТС, это "глобальные переменные, доступные откуда угодно". Да, эту проблему решает инкапсуляция. Только вот тоже плохо решает — все зависит от дисциплины программиста. Если я захочу, то инстанс (замыкание/класс/объект) я тоже по всей программе протащу. Собственно, вопрос здесь только в инкапсуляции и дисциплине доступа.


Все же явное протаскивание класса — гораздо лучше. Просто потому, что оно явное, и в том месте, где оно не нужно, его не будет. А глобальная переменная загаживает весь контекст.

M>>>Кстати, как вы хотите бороться с "глобальным состоянием" там, где оно существует исходя из контекста задачи? Банальный почтовый клиент возьмите. Вот пришло пользователю новое сообщение. Это глобальное или локальное состояние?

ARK>>Локальное.
M>Почему? А если это singleton? А если это не singleton, но список писем можно вытащить из любого места программы (потому что "application state" у нас отсутствует только в самых харкдорных библиотеках)?

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

M>>>Ну и так же, как с глобальными переменными — мы не отказываемся от них, мы их инкапсулируем.

ARK>>Куда инкапсулируем, в синглтоны?
M>В инстансы, которые потом по программе таскаем . А вообще, наверное, вы правы. Стоит различать глобальные переменные в смысле "нечто, что существует все время работы программы" и в более узком "переменная, объявленная на самом верхнем уровне/в единственном контексте". Со вторыми можно достаточно успешно бороться. Правда, не всегда удобно (в типичном UI много с собой носить приходится).

Да, я именно про это. От первого никуда не уйти, а второе в топку.

M>Вот. Кстати, вы уверены, что на вызовах c(...) вы нигде коды ошибок не забываете обработать? Можно и через discriminated union вернуть, если память позволяет.


Если возврат через discriminated union, то забыть обработать не выйдет. Придется матчить тэг и смотреть, что вернулось.

M>Где здесь нелокальность???


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

M>>>P.S. Исключения — это локальный переход!

ARK>>Нет, не локальный, т.к. мы не знаем конечной точки.
M>Вызов полиморфного (вирутального/динамического метода) — нелокальный переход (мы не знаем конечной точки).

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

M>Хуже того, return — это нелокальный переход (по той же причине)! return нужно запретить!


return вызывает переход на следующую строку после вызова. Вполне определенная точка.

M>Если же подходить с точки зрения "структурного программирования" (есть конструкции с "одним входом и одним выходом") и исключения, и break/continue (частные случаи goto) являются структурными конструкциями!


Структурными, однозначно.

M>"Нелокальный переход" — это другое. Это "у нас выполнялась функция, а теперь вдруг перестала и об этом мы вообще никак узнать не можем".


Не понял, чуть подробнее можете расписать?
Кстати, педивикия так не считает: http://en.wikipedia.org/wiki/Control_flow#Structured_non-local_control_flow
Re[7]: Нелокальные управляющие конструкции считать вредными
От: FR  
Дата: 26.03.13 05:47
Оценка: :)
Здравствуйте, AlexRK, Вы писали:


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


Надо идти дальше из управляющих конструкций оставить только условный оператор и рекурсию.
Re[8]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 26.03.13 07:45
Оценка: :)
Здравствуйте, FR, Вы писали:

ARK>>Кстати, мне думается, что ленивость можно сделать без yield return. Для программиста это будет выглядеть как мой код, но вместо List<TreeNode> будет нечто вроде LazyList<TreeNode>. Умный компилятор перепишет это в такой же ленивый генератор.


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

FR>разыменовывать как указатели, например такие ленивые типы есть в OCaml и F#.

Прошу прощения, не понял, можете пояснить? О чем должен догадаться компилятор?

FR>Но очень большой плюс yield кроме ленивости это возможность легко комбинировать

FR>функции и использовать рекурсию, во многом аналогично ФВП в функциональных языках.
FR>Например в питоне набор функций для итераторов http://docs.python.org/2/library/itertools.html

Посмотрел, но, по-моему, тут yield return опять же только ленивости везде. Все функции и без него переписываются 1-в-1 с обычными контейнерами. Я не прав?
К примеру,
def takewhile(predicate, iterable):
    # takewhile(lambda x: x<5, [1,4,6,4,1]) --> 1 4
    for x in iterable:
        if predicate(x):
            yield x
        else:
            break

меняем на
IEnumerable<T> takewhile<T>(Predicate<T> predicate, IEnumerable<T> iterable)
{
  var result = new List<T>();

  foreach (var x in iterable)
  {
    if (predicate(x))
      result.Add(x);
    else
      break;
  }

  return result;
}


FR>При этом выразительность получается вполне на уровне ФВП, как пример можно посмотреть

FR>вот эту библиотечку для питона https://github.com/JulienPalard/Pipe .

Посмотрел. Вроде тут везде просто применение уже готовых функций из itertools.
Re[9]: Нелокальные управляющие конструкции считать вредными
От: FR  
Дата: 26.03.13 08:30
Оценка: :)
Здравствуйте, AlexRK, Вы писали:

ARK>Ну, это перебор. Я предлагаю минимизировать не число управляющих конструкций вообще, а число гото-подобных. Кстати, вот в Eiffel нет return, break и continue. А это все же не совсем маргинальный язык, хотя и не мейнстримовый.


Я против полумер
Интересный язык кстати может получится из трех только вещей: условный оператор, рекурсия и undelimited continuations
Re[11]: Нелокальные управляющие конструкции считать вредными
От: FR  
Дата: 26.03.13 09:17
Оценка: +1
Здравствуйте, AlexRK, Вы писали:

ARK>Эээ... а название типа ему в этом не поможет? Если функция возвращает List, то код энергичный, если LazyList — то делается генератор а-ля yield.

ARK>А на вызывающей стороне должно быть пофиг.

В таком случае получаем тот же yield вид сбоку.
Но менее удобный, смотри тот же BatEnum.
Менее удобный потому что yield позволяет передать управление не теряя контекста,
в этом он похож чем-то на замыкания. Без него придется все делать ручками и вместо
простого и наглядного:
def Fib(n):
    a, b = 0, 1
    for i in xrange(n):
        yield a
        a, b = b, a + b

мы получим такое страшилище:
class Fibonacci:
  def __init__(self, max):
    self.n, self.a, self.b, self.max = 0, 0, 1, max
  def __iter__(self):
    return self
  def next(self):
    if self.n < self.max:
      a, self.n, self.a, self.b = self.a, self.n+1, self.b, self.a+self.b
      return a
    else:
      raise StopIteration



ARK> А где квадратичная? По памяти в 2 раза больше, по времени то же самое. Нет?


Да тут я дал маху, но N прогонов по циклу вместо одного в цепочке из N функций
все равно получим.

ARK>Дык, мы же с этого начали — я сделал предположение, что с сферическим умным компилятором усложнения быть не должно (просто взять LazyList вместо List).


Не вижу как можно сделать такой компилятор.
Re[9]: Нелокальные управляющие конструкции считать вредными
От: maxkar  
Дата: 27.03.13 15:03
Оценка: +1
Здравствуйте, AlexRK, Вы писали:

ARK>Здравствуйте, maxkar, Вы писали:


ARK>Да нет же. Добавление элемента в массив выполняется за линейное время (амортизированное).

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

ARK>А, ну да. Но вообще это тонкий момент. Я, например, когда смотрю код, я не вижу, что функция "монадная".

Ну да. Приходится помнить, что в этом языке у нас все "монадное". И без этого никак. Если убрать монадность, будет другой язык. Для восприятия разница есть только тогда, когда мне нужно в конце какое-нибудь гарантированное действие выполнить (освободить ресурсы). Там приходится помнить, временем жизни каких ресурсов нужно вручную управлять, а какими — нет. Появилась идея (может быть, бредовая, я ее пока не продумывал). Может быть, стоит явно помечать необходимость ручного управления ресурсами для какого-то класса (т.е. необходимость вызвать "деструктор"). При использовании либо проверять гарантированный вызов метода деструкции (не смотря на исключение), либо требовать пометить работу с ресурсом явно как "небезопасную". Это еще упростит ряд сценариев. Останутся те, где нужно обеспечивать целостность данных в случаях ошибки, их автоматически пока проверять не получится.

ARK>Мы же не говорим, что if — это goto, несмотря на то, что реализовано оно именно так?

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

M>>Так что я за сохранение и приумножение разнообразия различных моделей вычислений. Естественно, без лишнего фанатизма.

ARK>Дык — контроль нужен, вот в чем дело. Желательно формальный. Иначе кул ксакепы в порыве самореализации наплодят ужасов и это расползется по библиотекам.

Но контроль должен быть удобным! А с этим не все так просто. Мешают те самые "формальные проверки". Например, я могу из функции, работающий с "обычными" значениями сделать функцию, работающую с "реактивными" значениями. Там кроме применения функции ничего нет. Аналогично с асинхронными операциями. Может быть, вообще "абстрагировать" модель вычисления функции от рантайма. И одну и ту же функцию (внутреннее представление) уметь компилировать и в обычные функции, и в реактивные, и в асинхронные. Т.е. уже от контекста используемых "значений" выбирать правильную модель выполнения функций. Это вроде бы должно работать для "чистых" функций. И проверку "типов" сделать более гибкой. Не "системой типов" проверять, а скриптом в каждом конкретном месте, передавая актуальные типы аргументов (это для магии на javascript нужно).

Так что контроль — палка о двух концах. Либо он сложный в использовании, либо проверяет слишком мало. Какие-то ужасы могут родиться для обхода контроля (вот, собственно). Да и большинство ужасов происходит не столько на уровне кода, сколько на уровне "архитектуры". Код внутри методов при нормальн8ой архитектуре можно и поправить. А вот наоборот сложнее.
Re[13]: Нелокальные управляющие конструкции считать вредными
От: maxkar  
Дата: 27.03.13 15:17
Оценка: +1
Здравствуйте, AlexRK, Вы писали:


ARK>Элементарно же. Вот это

ARK>автоматом переписывается в

Не элементарно. Далеко не все операции "лениво" поддерживаются. Точно поддерживаются добавления одного элемента и ленивой последовательности. Но вот то же удаление элементов должно форсировать некоторые вычисления. Да и доступ к предыдущим элементам — тоже. Так что LazyList, вероятно, будет иметь очень специальный интерфейс.

И есть еще одна проблема:
var lazyList = new LazyList<String>()
var file = new File("someFile");
try {
  for (String line in file.lines())
    lazyList.add(file);
} finally {
  file.close();
}
return lazyList;


Вот это нельзя переписывать на ленивое возвращение. Никто не гарантирует, что итерация пройдет до конца. С yield подобные проблемы будет явно видно. На "обычном API" все так же проверяется, но заметить специальные случаи не так просто. Одно дело — конструкция языка, другое — почти обычные классы.
Re[21]: Нелокальные управляющие конструкции считать вредными
От: Sinix  
Дата: 29.03.13 08:42
Оценка: +1
Здравствуйте, AlexRK, Вы писали:

S>>Тоже не взлетит. (Источник и точные цифры не вспомню, но тут важно соотношение), 92% времени типового UI приложения — это ожидание ввода пользователя. Остальные 8% — дикое тормозилово из-за работы в UI-потоке. В случае с Metro UI время работы в UI-потоке уменьшается кардинально за счёт вытаскивания тормозов в await DoSomething(). Общее время выполнения при этом растёт (за счёт накладных расходов), зато отзывчивость UI практически идеальна.

ARK>Ну вот эти 8% и надо распараллелить.
Не надо — там логика в 99% случаем строго последовательна. Достаточно обрабатывать события асинхронно — например, вытащить обработку в фоновый поток, что не представляет особых проблем ещё с первого фреймворка.

ARK>Я думаю, будет нечто похожее на шейдеры — куча маленьких однопоточных программ.

Мошь шейдеров не в однопоточности, а в возможности параллельного выполнения последовательности операций над набором данных (aka SIMD). Часто такие вещи нужны в обычном бизнес-кода?

S>>Короче: для тяжёлых задач выбор практически не поменялся — или openMP и низкоуровневый хардкор на шейдерах, или специализированные скала с эрлангом, или умные балансировщики и куча мощных машин.


ARK>А с чего он должен был поменяться, если не существует коммерческих процессоров с миллионами ядер?

Эмм... а как вы собираетесь эти ядра сопрягать с внешними устройствами и разруливать доступ к памяти? Шины — они не резиновые вообще-то

ARK>640 кб хватит всем? Когда тактовую частоту наращивать больше не удастся, будут увеличивать число ядер. И что-то придумывать придется.


Ну блин! не нужно в 99% типовых сценариев массового распараллеливания. Смотрите закон Амдала — при сильной связности данных и при отсутствии однотипных вычислений распараллеливание становится неэффективным из-за затрат на синхронизацию. В оставшемся 1% случаев выигрыш достигается не банальным ключом компилятора "распараллель тут всё", а переводом всей модели вычислений на модель акторов или SIMD. Языки общего назначения для этого практически не подходят, специализированных решений — выше крыши. Как тут может кардинально поменяться ситуация?

Возьмёт самый примитивный сценарий — читалку/блокнот. Один поток для рендера UI, один поток для обработки очереди сообщений, поток для всякой логики и поток для фонового чтения файла. Все потоки спят 90% времени, сам рендер уже отдан GPU через DirectWrite/WPF. Чем тут поможет миллион ядер? Возможностью захостить на одной ферме миллион терминальных сессий, из которых 990 000 будут отрубаться из-за ширины канала?
Re[2]: Нелокальные управляющие конструкции считать вредными
От: Erop Россия  
Дата: 22.03.13 10:04
Оценка:
Здравствуйте, SergH, Вы писали:

SH>Как ты предлагаешь сделать это иначе?

1) Моё мнение состоит в том, что это удобнее делать через "почти глобальные переменные", и это тот редкий случай, когда подход оказывается выгодным. Тем не менее, в реальности часто хочется рулить логгированием как минимум по подсистемам, если не еёщ точнее, и на уровне приложения (глобальные переменные) многие тонкости, которыми хочется порулить при логгировании, просто неописуемы даже, а не то что бы удобно управляемы.

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

2) Если же говорить об изведении статических переменных вообще, как идеи, то была, например, когда-то такая СО симбиан, в которой статических переменных не было. И там всюду надо было передавать контекст вызова, через который можно было получить доступ к таким вещам, как логи или аллокации системной памяти...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[3]: Нелокальные управляющие конструкции считать вредными
От: SergH Россия  
Дата: 22.03.13 10:23
Оценка:
Здравствуйте, Erop, Вы писали:

E>1) Моё мнение состоит в том, что это удобнее делать через "почти глобальные переменные", и это тот редкий случай, когда подход оказывается выгодным.


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

E>так что логично иметь в программе сервис, которыей вешается на что-то вроде сессии, или приложения, и в этом сервесе регистрировать логгеры, и опции логгирования по подсистемам и далее иерархически вплоть до отдельных сложных классов, или границ с другими компонентами, если надо. Например если у нас программа как-то работет и что-то делает, но иногда грузит плагин, который, например, часть работы перекладывает на СУБД, то этот плагин и должен динамически расширять систему логгирования для специфического логгирования работы с СУБД. Опять же у СУБД могут быть свои логи, которыми тоже можно рулить и вообще интегрировать в своё приложение таким образом.


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

E>2) Если же говорить об изведении статических переменных вообще, как идеи, то была, например, когда-то такая СО симбиан, в которой статических переменных не было. И там всюду надо было передавать контекст вызова, через который можно было получить доступ к таким вещам, как логи или аллокации системной памяти...


Да всё можно конечно, но это же неудобно ужасно. Повышается количество кода, который копипастится из функции в функцию, все вызовы становятся многословнее. Искать содержательные части кода в общем объеме становится сложнее.
Зачем же так бороться с собой ради какого-то странно понятого принципа.
Делай что должно, и будь что будет
Re[4]: Нелокальные управляющие конструкции считать вредными
От: Erop Россия  
Дата: 22.03.13 10:39
Оценка:
Здравствуйте, SergH, Вы писали:

SH>Таких случаев можно придумать ещё несколько Например, выделение памяти. Пул подключений к базе. И т.п. Это то, что обычно называют политиками. Обычно это что-то такое, о чём программист, реализующий конкретное поведение, беспокоиться не хочет и не должен.


Ну да, но это надо ограниченно применять, ЭКОНОМИЧЕСКИ обосновав, не лепить направо и налево...
Ты мой ответ на стартовое сообщение читал?

SH>Это охрененно нелокальная конструкция Особенно с учётом того, что она будет управляться из конфига, и может быть даже перечитывать его налету.


Она, тем не менее, может быть спроектирован так, что её нелокальность будет доступна очень ограниченно. В конце концов сама аппаратура нелокальна, что не мешает нам, тем не менее, писать вполне локальные рограммы. Ну. по крайней мере, некоторым из нас

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


Не знаю. Как-то я переносил довольно большую и сложную AI хреновину на симбиан. Не сказал бы, что она на симбиане была сильно сложнее, чем на Win CE, например...
А если учесть, что со статическими переменными всё равно есть куча заморочек, то косяки под симбианом были радикально прогнозируемее, к тому же...

SH>Зачем же так бороться с собой ради какого-то странно понятого принципа.

В смысле "бороться с собой"? Бороться надо не с собой, а за удешевление поддержки и разработки при сохранении удобства и функционала...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[5]: Нелокальные управляющие конструкции считать вредными
От: SergH Россия  
Дата: 22.03.13 11:06
Оценка:
Здравствуйте, Erop, Вы писали:

E>Ну да, но это надо ограниченно применять, ЭКОНОМИЧЕСКИ обосновав, не лепить направо и налево...


Конечно ограниченно, не надо лепить всюду. Но и запрещать вообще это неоправданный экстремизм.
Я именно это имел ввиду под "борьбой с собой": мы видим, что так всем удобнее и проще, но из принципа будем делать иначе.

E>Ты мой ответ на стартовое сообщение читал?


Да, я согласен, что на исключениях не стоит делать сложную логику. Но иногда их использование полезно.
Можно конечно считать, что это очень опасный инструмент и не стоит давать его программисту в руки, порежется. Но это глупость, не нужно так делать. Как писал великий Страуструп в "Дизайн и эволюция С++"

Хорошие программы – это продукт хорошего образования, удачного проектирования, адекватного тестирования и т.д., а не наличия в языке средств, которые предположительно должны использоваться только «правильно». Употребить во вред можно любое средство, поэтому вопрос не в том, можно ли чем-то воспользоваться неправильно (можно!) или будут ли этим неправильно пользоваться (будут!). Вопрос в том, необходимо ли данное средство при условии его правильного употребления настолько, чтобы оправдать затраты на его реализацию, удастся ли смоделировать его с помощью других средств языка и реально ли с помощью обучения обратить во благо неправильное использование.

Делай что должно, и будь что будет
Re[6]: Нелокальные управляющие конструкции считать вредными
От: Erop Россия  
Дата: 22.03.13 11:11
Оценка:
Здравствуйте, SergH, Вы писали:

SH>Я именно это имел ввиду под "борьбой с собой": мы видим, что так всем удобнее и проще, но из принципа будем делать иначе.


Часто всё-таки есть конфликт интересов между "всем удобнее и проще", и "выгоднее для бизнеса"...
Поэтому в подобных случаях ЭКОНОМИЧЕСКОЕ и ТЕХНИЧЕСКОЕ обоснования таки требуются, а по умолчанию, без обонований -- запретить.

SH>Можно конечно считать, что это очень опасный инструмент и не стоит давать его программисту в руки, порежется. Но это глупость, не нужно так делать. Как писал великий Страуструп в "Дизайн и эволюция С++"

Как много удачного и ДЕШЁВОГО в разработке коммерческого ПО наманагерил "великий Страуструп" в своей жизни?..

SH>

Хорошие программы – это продукт хорошего образования, удачного проектирования, адекватного тестирования и т.д., а не наличия в языке средств, которые предположительно должны использоваться только «правильно». Употребить во вред можно любое средство, поэтому вопрос не в том, можно ли чем-то воспользоваться неправильно (можно!) или будут ли этим неправильно пользоваться (будут!). Вопрос в том, необходимо ли данное средство при условии его правильного употребления настолько, чтобы оправдать затраты на его реализацию, удастся ли смоделировать его с помощью других средств языка и реально ли с помощью обучения обратить во благо неправильное использование.


С этим я и не спорю. Вопрос всего лишь о дефолтах и процедурах по отступлению от дефолтов...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re: Нелокальные управляющие конструкции считать вредными
От: Ikemefula Беларусь http://blogs.rsdn.org/ikemefula
Дата: 22.03.13 11:48
Оценка:
Здравствуйте, Tilir, Вы писали:

T>Сложность жизненного цикла программы это не только сложность разработки, но и сложность поддержки. Большинство признанных сегодня методов разработки, таких как структурное программирование, ООП и паттерны проектирования, etc направлены на борьбу со сложностью и делают разрабатываемые системы более прозрачными, более соответствующими Principle Of Least Astonishment [5], и в конечном итоге более простыми в поддержке. В противовес этому, нелокальные управляющие конструкции дают прирост скорости разработки за счёт потери детерминированности и простоты разрабатываемого приложения.


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

T>В функциональных языках нелокальные конструкции, такие как pattern matching или ленивые списки, не вызывают особой боли, потому что являются естественной частью non-modifyable мира, где модификация загнана внутрь монад и интеллектуальные ресурсы разработчика не расходуются на поддержку консистентности модифицируемых структур данных при нелокальных переходах. Но по многим причинам (в частности из-за высокого входного порога и принципиально низкой производительности на существующих архитектурах -- представьте, например, что вместо модификации одного узла дерева, вы полностью уничтожаете и создаёте заново весь миллион его узлов), такие языки до сих пор представляют скорее академический интерес. Что касается нелокальных конструкций в модифицируемом мире, то мы должны отказаться либо от них, либо от оператора присваивания. К тому, же в мире чисто функционального программирования, детерминированности остаётся ещё меньше.


Почему PM это нелокальная конструкция ?

T>При проектировании надёжных, стабильно работающих приложений, мы должны, уже на этапе выбора архитектуры, инструментов и методологий, сводить к минимуму дорогостоящую отладку и поддержку на поздних этапах существования продукта. Речь не идёт о преждевременной оптимизации. Речь идёт о принципиальном отказе от недетеринизма, так же, как мы отказываемся от нелокальных переходов и убираем зависимости от глобальных переменных.


T>Именно поэтому я предлагаю все нелокальные управляющие конструкции считать вредными.


Это похоже камень в огород функциональщине и метапрограммированию ?
Re[8]: Нелокальные управляющие конструкции считать вредными
От: Erop Россия  
Дата: 22.03.13 11:49
Оценка:
Здравствуйте, SergH, Вы писали:

SH>Мне сложно представить себе экономическое обоснование на использование/запрет чего-либо. Это что-то типа



Дык нет исследований -- нет и обоснования...
Либо выгода ОЧЕВИДНА и обоснование написать легко.

А так, конечно, принимать решения за которые отвечает кто-то другой не сложно
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 22.03.13 12:58
Оценка:
Здравствуйте, Tilir, Вы писали:

В целом согласен. Я ненавижу исключения и прочие break-continue.

Но вот не знаю — как быть без исключений, если хочется малой кровью написать относительно живучую программу. К примеру, падение в каком-то методе вызывает откат до определенной точки "вверху" и пользователю отображается сообщение. Ведь даже банальное i := i + 1 может вызвать переполнение. И таких функций с ограниченной областью определения в программах море. Все это — скрытые баги. Если исключения убрать, то есть только два варианта, как поступить — либо любая ошибка вызывает останов программы (как в С или Oberon), либо мы должны статически доказать, что ошибки быть не может (что при текущем уровне развития технологий требует кучи дополнительного кода и является в общем случае алгоритмически неразрешимой задачей).
Re[3]: Немного шуток и прибауток AKA злостный офтоп ;)
От: Erop Россия  
Дата: 22.03.13 15:20
Оценка:
Здравствуйте, denis8158, Вы писали:

D>Типун Вам на язык, чтобы такое было в моём благоверном C#....


И тада прибор нам, как жена
И тада жена нам не нуна
И тада любой из нас не против
Дни и ночи проводить в работе?..
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[2]: Нелокальные управляющие конструкции считать вредными
От: FR  
Дата: 23.03.13 16:34
Оценка:
Здравствуйте, VladD2, Вы писали:

T>>* Нелокальный GOTO

T>>* Continuations
T>>* Исключения
T>>* Сигналы
T>>* Нотификации
T>>* Сопрограммы
T>>* Генераторы
T>>* Нити исполнения
T>>... имя им легион ...

VD>Нелокальный GOTO, исключения и потоки несомненно создают проблемы.


Continuations тоже.
Во первых можно сломать мозг
Во вторых наломать дров на продолжениях без проблем.
Но как и с макросами по моему польза перевешивает вред.
Re[2]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 23.03.13 17:57
Оценка:
Здравствуйте, maxkar, Вы писали:

С вашего позволения прокомментирую.

M>Нет. Нити не предлагают противоположный подход. Нити появляются там, где "недетерминизм реально нужен". Обработка данных на нескольких ядрах, параллельное выполнение блокирующих и неблокирующих операций (чтение данных и пользовательский UI) и т.п. А вот уже после того, как нити стали нужны, приходится бороться с недетерминизмом и всем остальным.


Нити не нужны вообще, это ошибка эволюции. Нужны легковесные процессы, как в Singularity. И там общение с дочерними процессами вполне детерминировано.

T>>* Continuations

M>Полезны в определенных случаях. Какие-нибудь data flow строить, например. Еще лучше — динамически строить тот же data flow. Вручную перекладывание данных в "процедурном стиле" из одного источника в другой будет ничуть не лучше.

Пример не покажете?

T>>* Сопрограммы

T>>* Генераторы
M>Это которые yield return? Так удобно же! Код "генератора вручную" читать и писать гораздо тяжелее, чем yield return.

Писать может и удобно, а вот читать — не уверен.

M>Да пожалуйста. Приведите аналог "нитей исполнения" в локальном и детерминированном эквиваленте. И желательно в контексте, когда в одном потоке мы читаем файл, а второй обрабатывает события пользовательского интерфейса (хотя бы только перерисовку, ладно уж).


Каналы процессов (http://www.rsdn.ru/article/singularity/singularity.xml
Автор(ы): Galen Hunt, James Larus, Martin Abadi, Mark Aiken, Paul Barham, Manuel Fahndrich, Chris Hawblitzel, Orion Hodson, Steven Levi, Nick Murphy, Bjarne Steensgaard, David Tarditi, Ted Wobber, Brian Zill
Дата: 02.03.2006
Singularity – исследовательский проект Microsoft Research, который начался с вопроса: на что была бы похожа программная платформа, если спроектировать ее на пустом месте, и во главу угла поставить не производительность, а надежность?
).

M>Да. Вопрос только — какой ценой. Вот, пожалуйста, пример. Async.applySync(someFunction, [loadDataA, loadDataB, preloadImages]). Эта страшная штука применяет someFunction после того, как будут загружены данные A, B, и предзагружены картинки. Вся загрузка — асинхронная (сетевая) и параллельная. Это не совсем continuations, (параметров многовато), ну да ладно. Для усложнения ситуации там до этого вполне может быть loadDataB = Async.applyAsync(loadDataB, [loadDataA, loadDataC]), т.е. данные B загружаются после загрузки данных A и C (мало ли что там, ссылки на другие данные, например).


Эта "страшная штука" — опять те же нити. Причем тут continuation? Дайте пример без async.

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


Для нечистых тоже можно (см. ту же Singularity). По утверждению авторов контракты каналов позволяют проверить даже наличие deadlock-ов.

M>Да нет, не только разработки. Чтения тоже (yield return читать проще, чем большой и страшный автомат).


Не спора ради, а истины для. Не приведете пример убедительного превосходства yield return?

M>Кстати, вы от OOP и наследования от интерфейсов тоже предлагаете избавится? Они же недетерминизм вносят (как только мы dependency заинжектили — все, детерминизм кончился).


Не понял, какой здесь вносится недетерминизм? Если контракты интерфейса соблюдены, то все детерминировано. Кстати, интерфейсы бывают не только в C# или Java. Eiffel или выброшенные из С++ концепты предлагают более широкие возможности — предусловия-постусловия, аксиомы.

M>И касается это в первую очередь внешнего API... Не кода, не того, что и куда там переходит. А внешнего API.


Ну уж нет. Спагетти-код никакое API не вытянет.

M>А вот здесь вы ошибаетесь. Нельзя выкинуть глобальные переменные. И никто этого не делает. Обычно их инкапсулируют во что-нибудь менее страшное.


Есть языки без глобальных переменных (Eiffel).

M>Поэтому и работать с ними приходится. А давайте мы еще пару "безопасностей" добавим — безопасность относительно освобождения ресурсов и безопасность относительно гарантии обработки всех ошибок. Тоже откажемся? А ведь освобождение ресурсов это вообще частный случай "глобальных переменных"! Т.е. мы откажемся от работы с памятью (можно попробовать на GC поменять), работы с файлами и от всего остального.


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

M>Кстати, как вы хотите бороться с "глобальным состоянием" там, где оно существует исходя из контекста задачи? Банальный почтовый клиент возьмите. Вот пришло пользователю новое сообщение. Это глобальное или локальное состояние?


Локальное.

M>Или нам весь мир нужно перестроить (да, уже там все будет достаточно сложно, потому что событие произошло далеко внизу и придется распутывать сплетения монад друг над другом). А самое интересное, что в процессе "перестроения мира" выяснится, что пользователь подвел курсор мышки к кнопочке и у нас анимация играется уже 0.2 секунды (и осталось еще 0.4 секунды проигрывания).


Ничего не понял, зачем чего-то перестраивать.

M>Да и на экране нужно вывести что-нибудь, а это "глобальное состояние" ОС, монитора и пользователя, так что еще и все состояние OS нужно куда-нибудь передать и создать.


Это опять внешний мир, а не глобальные переменные.

M>Ну и так же, как с глобальными переменными — мы не отказываемся от них, мы их инкапсулируем.


Куда инкапсулируем, в синглтоны?

M>Исключения — замена кодам ошибок. Иногда — достаточно хорошая. Потому что иногда приходится возвращать сложную структуру. Или там long long произвольный. Как вы его на кодах ошибок вернете? Через глобальные переменные (от которых мы отказались)? Или через выделение структуры?


Через discriminated union.

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


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

M>А локальные переменные (которые мутабельные) вы еще не предлагали запретить? Про них можно все те же аргументы привести, что и про глобальные переменные (только чуть в меньшем масштабе).


"Те же аргументы" можно привести только если у вас метод длиной в тысячи строк. В ином случае это малость неадекватно.

M>P.S. Исключения — это локальный переход!


Нет, не локальный, т.к. мы не знаем конечной точки.
Re[3]: Нелокальные управляющие конструкции считать вредными
От: maxkar  
Дата: 24.03.13 16:31
Оценка:
Здравствуйте, AlexRK, Вы писали:

ARK>Нити не нужны вообще, это ошибка эволюции. Нужны легковесные процессы, как в Singularity. И там общение с дочерними процессами вполне детерминировано.

А как там interpocess communication реализуется? Мне в идеале хочется гонять структуры между различными частями программы (теми самыми легковесными процессами). Иногда — достаточно большие объемы. Ну вот банальщину возьмем. Игру какую-нибудь сетевую. Один поток ведет сетевой обмен. Второй поток обсчитывает механику (очевидно, должен получать сообщения от сети в удобоваримом виде). Третий поток картинку генерирует к физике. И системный процесс все это безобразие на экран выводит (double buffering в отрисовке). В зависимости от удобства API мы получим либо те же shared memory со всеми ее проблемами (buffered image передавать надо как-то), либо кучу неудобств на ровном месте. Ну или дайте ссылку на конкретный API, я посмотрю. Если что, "высокоуровневые" примитивы обмена ведь и с обычными потоками можно использовать.

Хотя, в общем, я ничего против замены потоков на легковесные процессы не имею. При удобном API это действительно может быть одним и тем же.

T>>>* Continuations

ARK>Пример не покажете?

Ну... С одной стороны, это все те же yield. Это из общего соображения. cps (continuation passing style) — обработка данных. Наиболее часто — вывод мультимедиа (сбор графов на источнике/получателе данных). Я что-то подобное делал, когда данные из базы нужно было загружать, обрабатывать и выгружать. То ли база данных для lucene собиралась, то ли что-то еще. С точки зрения интерфейса чтения данных из базы, все остальное за ним — continuation...

ARK>Писать может и удобно, а вот читать — не уверен.

Удобнее, чем явный автомат. Как минимум в типичных случаях. Можно, конечно, и ужасы написать. Но от misuse никто не застрахован.

ARK>Каналы процессов (http://www.rsdn.ru/article/singularity/singularity.xml
Автор(ы): Galen Hunt, James Larus, Martin Abadi, Mark Aiken, Paul Barham, Manuel Fahndrich, Chris Hawblitzel, Orion Hodson, Steven Levi, Nick Murphy, Bjarne Steensgaard, David Tarditi, Ted Wobber, Brian Zill
Дата: 02.03.2006
Singularity – исследовательский проект Microsoft Research, который начался с вопроса: на что была бы похожа программная платформа, если спроектировать ее на пустом месте, и во главу угла поставить не производительность, а надежность?
).

Ок. Хорошо. Мы можем в некоторых местах перейти на каналы. Если эффективность нас не особо волнует, наверное, мы много чего сможем на них сделать. В голову вроде бы ничего не приходит невозможного. Остается вопрос эффективности. Например, у нас 10 игроков. Мы генерируем уникальные идентификаторы каких-то сообщений (того же чата, например, только не спрашивайте, зачем мы это делаем...). Я правильно понимаю, что без отсутствия shared state мы у специального процесса будем запрашивать генерацию и ожидать ответ на канале?

Собственно, все это можно делать уже и сейчас. Есть же очереди сообщений. Да, у нас не легковесные процессы, у нас потоки. Но примитивы те же самые (блокирующие типизированные очереди). Но вот ведь беда, иногда требуется эффективность выше. Начиная от простейщего incrementAndGet и заканчивая сложными мутабельными структурами (вроде nonblocking list/nonblocking map). Ведь не от хорошей же жизни всем этим пользуются... А где можно не пользоваться сложными структурами (и обеспечивать необходимую безопасность), ими и не пользуются.

M>>Async.applySync(someFunction, [loadDataA, loadDataB, preloadImages]).

ARK>Эта "страшная штука" — опять те же нити. Причем тут continuation? Дайте пример без async.
Вот в данном то конкретном случае нитей там и не было Это полностью однопоточный код (и вообще там в рантайме поддержки потоков нет!). Где-то на верхнем уровне он вырождатеся в тот самый цикл на сообщениях:
while (true) {
  case (waitForNextMessage()) {
    case WM_RENDER:
      fireNextFrameListeners();
      renderNextFrame();
    case WM_NET_EVENT(e):
      var netQueue = findNetQueue(e.queueId);
      netQueue.appendData(e.data);
      ...
    case WM_MOUSE_EVENT(e):
      ...
  }
}

Собственно, async он далеко не потому, что выполняется в другом потоке. Async он потому, что актуальная функция (someFunction) выполняется уже после того (в общем случае), как выполнилась Async.applySync (и вооще через несколько итераций цикла обработки сообщений). someFunction — типичная continuation (она и методом объекта может быть, и всем остальным). Выполняется "по завершению" loadDataA, loadDataB, preloadImages.

Да в общем, это и не важно. Как бы вы подобный код написали в однопоточном виде? Ну пусть через те же каналы. В некоторых случаях — через waitForMultipleObjects. Только не везде можно было бы обойтись этим (все равно и каналы новые нужно создавать, и запросы отправлять). Да, я писал вначале "ручные" автоматы (if loadState == ... и if loadedCount == ...). Когда переписал на continuations + async, вздохнул с огромным облегчением.

Типичный метод:
public function doSomething(app, arg1, arg2) : OperationState<Void> {
  var dataA = loadSomethingVeryAsync(arg1);
  var dataB = loadSomethingOverAsync(arg2);
  var preloadedImages = Async.applyAsync(preloadImagesForData, [dataA]);
  var externalData = Async.applyAsync(loadDataFromExternalSystem, [dataB, arg2]);
  var appUI = Async.applySync(createUI, [app, dataA,  extenralData, preloadedImages]);
  return Async.applySync(app.addDialog, [appUI]);
}

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

И еще. Обратите внимание на уровень декларативности кода. Ему совершенно без разницы, на потоках происходит вся асинхронность или "в цикле на событиях"! Еще оно прозрачно и ошибки возвращает (ну правда, не вручную же на каждом канале проверять, успешно все закончилось или нет). А все — на continuation (потому что addDialog, createUI, loadDataFromExternalSystem — это некое continuation в самом широком смысле слова).

ARK>Для нечистых тоже можно (см. ту же Singularity). По утверждению авторов контракты каналов позволяют проверить даже наличие deadlock-ов.

Так там же нет "детерминированного параллелизма"! Там вполне себе недетерминированный параллелизм и обработка очереди сообщений. Будет ли там в очередь посылать поток или процесс, не слишком важно. Важно наличие "недетерминизма". ОП вообще говорил про параллелизм в стиле MPI. Тот параллелизм вообще в основном для обработки больших-больших матриц предназначен.

Отсутствие deadlock — это хорошо (без иронии). Может ли singularity гарантировать, что обработчик сообщений справится со всем потоком (т.е. очередь не будет расти бесконечно)? Вопрос серьезный. Например, для "автоматического парралелизма в стиле MPI" это чисто теоретически можно доказать. А иначе опять руками все доказывать...

И еще раз замечу, что моему коду выше (на continuation passing в async комбинатор) никакого дела до физической реализации асинков нет. Могу на процессы переписать, могу на каналы. И для каналов таки напишу аналог, если придется (потому что короче, чем на автоматах врукопашную).

ARK>Не спора ради, а истины для. Не приведете пример убедительного превосходства yield return?

Ну что-то вроде:
def listInTree(curNode, pred):
  if pred(curNode):
    yield return curNode
  else:
    for sub in curNode.children():
      for res in listInTree(sub, pred)
         yield return res

Можно еще всякие графы более сложные обходить, например. Но это так, навскидку. Я на языки с yield return совсем недавно освоил, поэтому ничего сильно лучше под рукой нет.

M>>Кстати, вы от OOP и наследования от интерфейсов тоже предлагаете избавится? Они же недетерминизм вносят (как только мы dependency заинжектили — все, детерминизм кончился).

ARK>Не понял, какой здесь вносится недетерминизм?
Ну как... В вашем же определении "Нет, не локальный, т.к. мы не знаем конечной точки." А конечной точки мы не знаем
ARK>Если контракты интерфейса соблюдены, то все детерминировано. Кстати, интерфейсы бывают не только в C# или Java. Eiffel или выброшенные из С++ концепты предлагают более широкие возможности — предусловия-постусловия, аксиомы.

Очень хорошо. А кто в этих случаях аксиомы доказывает? Пользователь или компилятор? Но даже это не важно. Вот объясните мне, почему мы можем проверить "контракты соблюдены", а "метод выбрасывает только исключения из списка ..." не можем? А если можем проверить "список исключений", то какие тогда вообще проблемы? Исключение — это не переход. Исключение — это результат работы метода! Монада такая. Все, после этого мы все, что угодно, можем доказывать.

M>>А вот здесь вы ошибаетесь. Нельзя выкинуть глобальные переменные. И никто этого не делает. Обычно их инкапсулируют во что-нибудь менее страшное.

ARK>Есть языки без глобальных переменных (Eiffel).
Это вряд ли. Переменная в замыкании примерно такая же глобальная переменная, как и все остальные. То, о чем говорил ТС, это "глобальные переменные, доступные откуда угодно". Да, эту проблему решает инкапсуляция. Только вот тоже плохо решает — все зависит от дисциплины программиста. Если я захочу, то инстанс (замыкание/класс/объект) я тоже по всей программе протащу. Собственно, вопрос здесь только в инкапсуляции и дисциплине доступа.

ARK>Не думаю, что взаимодействие с внешним миром стоит приравнивать к глобальным переменным. Мир — это не глобальная переменная, он меняется независимо от программы. Уйти от взаимодействия с ним обычно нельзя. А глобальные переменные просто ухудшают структуру программы, ничего не давая взамен (ну точнее они дают легкость написания неподдерживаемого быдлокода ).


Стоит, стоит мир рассматривать как глобальную переменную. Ведь можно в файлик записать в одном месте программы а потом в другом месте попробовать считать. Да, конечно, мир еще менее детерминированный, чем глобальные переменные, ну так и что?

Лирическое отступление по внешнему миру. Одно время (еще студентом) будучи студентами мы как-то попрбовали зареверсить msgina.dll (за аутентификацию пользователей отвечает). Хотели посмотреть на сообщение "logout denied" (да, logout, оказывается, может не сработать!). И вот там в процедуре входа пользователя в систему был поразительный фрагмент. Он читал число из реестра и пытался вызвать по этому адресу процедуру! Может, конечно, мы это отреверсили неправильно, но мораль такая — мир нельзя исключать из рассмотрения (за исключением специальных случаев).

M>>Кстати, как вы хотите бороться с "глобальным состоянием" там, где оно существует исходя из контекста задачи? Банальный почтовый клиент возьмите. Вот пришло пользователю новое сообщение. Это глобальное или локальное состояние?

ARK>Локальное.
Почему? А если это singleton? А если это не singleton, но список писем можно вытащить из любого места программы (потому что "application state" у нас отсутствует только в самых харкдорных библиотеках)?

M>>Или нам весь мир нужно перестроить

ARK> Ничего не понял, зачем чего-то перестраивать.
Это если инкапсуляция хорошая. А так ведь у нас какое-нибудь UI завязано на "глобальное состояние" списка писем и еще на подобную кучу "локальных" состояний кнопок. В общем, отказ от глобальны переменных в узком смысле "глобальная переменная в стиле c" ничего не даст, все равно их можно изобразить (и глобальное состояние программы так или иначе где-то существует).

M>>Да и на экране нужно вывести что-нибудь, а это "глобальное состояние" ОС, монитора и пользователя, так что еще и все состояние OS нужно куда-нибудь передать и создать.

ARK>Это опять внешний мир, а не глобальные переменные.
Так он по всем свойствам (общая точка доступа, единственный экземпляр) как раз и является типичной глобальной переменной. И все техники по работе с ним похожи на техники работы с глобальными переменными. Так что без них никуда...

M>>Ну и так же, как с глобальными переменными — мы не отказываемся от них, мы их инкапсулируем.

ARK>Куда инкапсулируем, в синглтоны?
В инстансы, которые потом по программе таскаем . А вообще, наверное, вы правы. Стоит различать глобальные переменные в смысле "нечто, что существует все время работы программы" и в более узком "переменная, объявленная на самом верхнем уровне/в единственном контексте". Со вторыми можно достаточно успешно бороться. Правда, не всегда удобно (в типичном UI много с собой носить приходится).

M>>Как вы его на кодах ошибок вернете?

ARK>Через discriminated union.
M>>Очевидно, что структура будет выделяться на стороне вызывающего (потому что внутри функции память может не выделиться, а код ошибки мы вернуть не можем ).
ARK>Причем тут выделение вообще, это другой вопрос. Выделено может быть и на стеке. При проблемах с выделением памяти исключения ничем не лучше (а точнее гораздо хуже).

Не другой. Вопрос примерно такой же, как и с исключениями. В зависимости от механизма вашего discriminated union я буду выбирать механизм для discriminated union в случае исключений. "Исключения" в общем случае тоже "можно выделять на стеке". OutOfMemoryException можно и заранее выделить (и на стек забить, раз уж все так плохо). Так что OutOfMemory всегда есть. А все остальное ровно через те же механизмы и переписывается автоматически. Точно так же, как на кодах возврата. Только автоматически. Что-то вроде:
int someFunction(res, &abrupt) {
a(1,2,3);
b(2,3,4);
return c(d(3), r(5));
}
// переписывается в 
int someFunction(res, &abrupt) {
var state = Normal;
var rd, rr, res;
a(1, 2, 3, &state);
if (!isAbruptCompletion(state))
  b(2, 3, 4, &state)
if (!isAbruptCompletion(state))
  rd = d(3, &state);
if (!isAbruptCompletion(state))
  rr = r(5)
if (!isAbruptCompletion(state))
  res = c(rd, rr);
(*abrupt) = *state;
return res;
}

Вот. Кстати, вы уверены, что на вызовах c(...) вы нигде коды ошибок не забываете обработать? Можно и через discriminated union вернуть, если память позволяет. Где здесь нелокальность???

M>>P.S. Исключения — это локальный переход!

ARK>Нет, не локальный, т.к. мы не знаем конечной точки.
Вызов полиморфного (вирутального/динамического метода) — нелокальный переход (мы не знаем конечной точки). Хуже того, return — это нелокальный переход (по той же причине)! return нужно запретить! Только вглубь методов (continuation passing нас спасут, потому что больше ничего не осталось, а вы говорите — continuation зло!). Кстати, continuation (которые через continuation passing, а не через dsl (yield — частный случай dsl для генераторов)) очень и очень близки к полиморфизму. И при заворачинваии в интерфейс именно им и являются.

Если же подходить с точки зрения "структурного программирования" (есть конструкции с "одним входом и одним выходом") и исключения, и break/continue (частные случаи goto) являются структурными конструкциями! Только при этом тип каждого выражения/statement не такой, как записан, а монадный. Either<AbruptCompletion, A>. Да, превая часть не пишется, но всегда есть. И пролет исключения "мимо" метода все равно выходит из всех блоков (с Left<SomeException>) и из метода (с тем же Left<SomeException>). Вон, выше показал пример. Так что уж в исключениях то ничего плохого нет. Это вполне детерминированная раскрутка стека (с переходом в вызвавшую функцию) и вполне определенными механизмами взаимодействия с этой раскруткой. "Нелокальный переход" — это другое. Это "у нас выполнялась функция, а теперь вдруг перестала и об этом мы вообще никак узнать не можем".
Re[5]: Нелокальные управляющие конструкции считать вредными
От: maxkar  
Дата: 25.03.13 16:37
Оценка:
Здравствуйте, AlexRK, Вы писали:

ARK>вероятно там есть свои заморочки. Но вряд ли сопоставимые с геморроем с синхронизациями.

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

ARK>Фиг знает. Не нравятся мне разбросанные везде точки выхода.

Это дело привычки. "Точки выхода" разбросаны не везде, а в теле одного метода. И метод на самом деле немного специальный — вместо обычного типа возвращает генератор. Считайте, что это локальный язык такой.

M>>Например, у нас 10 игроков. Мы генерируем уникальные идентификаторы каких-то сообщений (того же чата, например, только не спрашивайте, зачем мы это делаем...). Я правильно понимаю, что без отсутствия shared state мы у специального процесса будем запрашивать генерацию и ожидать ответ на канале?

ARK>Ээ... ну наверное да. А как иначе-то. В принципе, получается та же блокировка, но реализованная на системном уровне.

Сейчас это принято делать неблокирующими вызовами. interlocked increment или как оно там правильно в процессорах зовется. Фокус именно в том, что это выполняется на уровне процессора (с обеспечением всех безопасностей). И никаких явных синхронизаций, ожиданий и т.п. Банальный atomicInt.incrementAndGet() в Singularity превращается в запрос состояния и обмен (хотя, может, там и есть что-то подобное, не читал я документцаию). При этом еще и с блокировками на очередях. А на incrementAndGet блокировок вообще нет. Она "за константное" время выполлняется. Примерно та же история и с nonblocking collections. Так что либо в singularity мы получаем опять проблемы с производительностью, либо все подобные техники мы можем в обычном языке (на потоках) делать. Хотя, например, в mainstream я uniqueness types не видел. Было бы интересно посмотреть, как на них atomicInteger записывался бы .

ARK>Э, нет. Если есть "цикл на сообщениях", это получаются те же самые нити, но реализованные вручную. Велосипед с квадратными колесами.

Ну не совсем с квадратными. Пользователя избавили от всех "сложностей многопоточности". Не нужны никакие критические секции, сложные инструкции вроде cmpxchg. На уровне стандартной библиотеки даже реентерабельность не нужна. Фактически, получилось примерно то, что происходит в singularity — обмен сообщенями через каналы. Между прочим, тут еще и очень важная гарантия проверяется: поток, обрабатывающий события UI, не блокируется на других каналах. Только вот видите, не удобно это все в "последовательном и безопасном" виде получилось. Все равно пришлось "потоки" на уровне внутреннего API изобрести.

ARK>А если код истинно однопоточный,

В строгом смысле слова он однопоточный. Обработка событий в одном конкретном месте. А вот "иллюзия многпоточности" как раз с использванием continuations. На безопасном API слишком неудобно. Я боюсь, что на практике singularity постигнет та же участь — будут добавлены "небезопасные", но удобные обертки.

ARK>Ну как, вот так как-то (псевдокод):

ARK>
ARK>  // child channel
ARK>  public Data GetData()
ARK>  {
ARK>    loadDataA();
ARK>    loadDataB();
ARK>    preloadImages();
ARK>  }
ARK>

К сожалению, это не эквивалент. Загрузка данных dataA и dataB должна идти параллельно. Это сокращает время ожидания пользователем доступности данных. Код — пример того, что "безопасный уровень" далеко не всегда удобен, а вот "continuations" в самом широком смысле — вполне нормальные. Я был бы рад, если бы из строго последовательный кода комплиятор преобразовал бы в то, что было в примере. Но, к сожалению, компилятор не умеет. Поэтому приходится вручную делать.

M>>Типичный метод:

ARK>Тут слишком много асинхронности для обычного метода. Это скорее какой-то диспетчер совсем верхнего уровня, каких в системе единицы.
Да какой это диспетчер? Каждый второй диалог примерно этим занимается. Если рассматривать только "крупные" диалоги, то вообще каждый первый! Картинки свои грузит, дополнительные данные, картинки для дополнительных данных и т.п. И это все только после того, как сам файл с кодом диалога загрузился. Вполне реальная задача, между прочим. Десяток подобных окон точно наберется. Может, даже и два десятка.

ARK>Вообще, больше обратил внимание на отсутствие типов данных и в связи с этим многое не понял. Что такое dataA, dataB — данные или методы?

public interface Reactive<T> {
  public T get();
  public void addChangeListener(...);
  public void removeChangeListener(...);
}

data CommState<a> = Error String | Progress Double | Result a;

var dataA : Reactive<CommState<Ta>>
var dataB : Reactive<CommState<Tb>>
var preloadImages : Reactive<CommState<Unit>>
var externalData : Reactive<CommState<ExtT>>
var appUI : Reactive<CommState<UIControl>>
function preloadImagesForData(a : TA) : Reactive<CommState<Unit>>{}
function loadDataFromExternalSystem(b : Tb, arg2: someType) : Reactive<CommState<ExtT>>
function createUI(app : Application, a : Ta, ext : ExtT, imagesLoaded : Unit) : UIControl
function app.addDialog(ui : UIControl) : Unit

Вот такая типизация. Извините, applySync/applyAsync типизировать не могу. Не знаю подходящей системы типов, в которой его можно было бы выразить достаточно точно. Похоже, математиков не очень интересуют объекты реального мира

ARK>Насчет continuation. Здесь разве есть continuation вообще? Continuation — это когда мы вызываем метод и больше не возвращаемся. А у вас обычные асинхронные вызовы.

Ну я с wiki брал определение.

In computer science and programming, a continuation is an abstract representation of the control state of a computer program. A continuation reifies the program control state, i.e. the continuation is a data structure that represents the computational process at a given point in the process' execution; the created data structure can be accessed by the programming language, instead of being hidden in the runtime environment.

Оно нечеткое. Считать ли обычную функцию класса "continuation" или нет? Считать ли, что только компилятор может создавать continuation или нет? Там в примерах есть continuation из scheme. Таких не пробовал, не знаю. Генераторы? Это некоторый сахар к порождающим функциям, я его не как "общий continuation" читаю. Скорее, он воспринимается как "асинхронный" producer и взаимодействие через ranvedous-point с потребителем.

Кстати, есть у меня и полностью однопоточный пример на "abstract representation of the control state of a computer program". Это push-parser комбинаторы. Т.е. не те, которые вызывал и получил результат, а которым можно по одному символу кормить и в конце концов что-нибудь из них извлечь. Вот оно в определенных местах примерно этим и кончается — мы получаем символ, вычисляем следующее состояние и передаем текущее состояние (частичный результат) следующему парсеру (или парсерам, если разбор недетерминированный). Только вот опять вопрос — считать ручное управление control state как continuation или нет? Или тот же fork/join параллелизм (мой пример с Async именно им и занимается, кстати)? Или только если он компилятором реализован?

M>>Отсутствие deadlock — это хорошо (без иронии). Может ли singularity гарантировать, что обработчик сообщений справится со всем потоком (т.е. очередь не будет расти бесконечно)? Вопрос серьезный. Например, для "автоматического парралелизма в стиле MPI" это чисто теоретически можно доказать. А иначе опять руками все доказывать...

ARK>Ну, такого рода доказательства возможны только в условиях серьезных ограничений (отсутствия динамически выделяемой памяти, сборщика мусора, рекурсии и много чего еще). В общем случае это невозможно.
Так зато безопасно! Или все-таки это создает слишком много неудобств? И за хорошими гарантиями гоняться не стоит? Но тогда чем в этом плане отличаются все якобы "нелокальные" переходы? Они еще ухудшают доказуемость (автоматическую безопасность) программ, но повышают удобство программиста.

ARK>
ARK>  List<TreeNode> ListInTree(TreeNode node, Predicate pred)
ARK>  {
ARK>    var result = new List<TreeNode>();

ARK>    if (pred(node))
ARK>    {
ARK>      result.Add(node);

ARK>      foreach (var childNode in node.Children)
ARK>      {
ARK>        result.AddRange(ListInTree(childNode, pred));
ARK>      }
ARK>    }

ARK>    return result;
ARK>  }
ARK>

ARK>По-моему, читабельность тут не особо ниже (если вообще ниже).
Угу. Только вот ваш код в худшем случае требует примерно в три раза больше памяти, чем мой. А так — все зависит от ветвистости дерева.

На самом деле еще хуже. Мой код требует памяти O(depth), где depth — глубина дерева (на самом деле поиска, я в дочерние узлы не захожу, если текущий узел удовлетворяет результату). Результат "разбирается" на месте. Ваш код требует O(depth) на стеке и еще O(result) для возврата результата. Там что-то порядка двоечки в O(result) входит, потому что на самом верхнем уровне рекурсии мы можем копировать почти весь результат с одной из веток. Да и со временем выполнения у вас не все гладко .

Пара проблем из обозначенных, конечно, решается. Нужно переписывать функцию на аккумулятор. Но тогда придется писать две функции, да и от лишних O(result) по памяти вы никуда не денетесь. Так что ваш код не эквивалентен моему. Он хуже по time/space complexity (преимущество у него будет только если стека больше, чем обычной памяти) и занимает больше кода.

ARK>А, в таком смысле. Тогда да. Но такой недетерминизм неизбежен, если есть полиморфизм в коде (любой).

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

ARK>Да, checked exceptions — доказуемы. Но вот беда, с ними возни столько, что польза становится сомнительной. Смысла в них особого нет, ИМХО. Сильно прозрачнее код они не сделают. Я это все к тому, что такие исключения — это и не исключения в общем-то (в том смысле, в каком они упоминаются в стартовом посте).


Почему это "не исключения в общем-то"? Ввели немного "дисциплины", и все. Точно так же переход выполняется "неизвестно куда". Вот что неудобные — согласен. Для них очень нужны discriminated unions (еще и открытые), union types и вывод типов исключений. А смысл там не в прозрачности. Смысл — в дополнительных гарантиях. Что определенные классы ошибок будут отловлены и соответствующим способом обработаны (да хотя бы во что-то другое завернуты). В определенных случаях и с дополнительными практиками вроде code review очень и очень хорошо помогают.

Кстати, если заменить весь ввод/вывод на асинхронный (т.е. приходят события о поступлении данных), то места исключениям сразу становится гораздо меньше. Они остаются только в парсинге пришедших данных. Всем остальным занимается большой и страшный Async-комбинатор. А все остальное — это уже ошибки в коде.

M>>Вот. Кстати, вы уверены, что на вызовах c(...) вы нигде коды ошибок не забываете обработать? Можно и через discriminated union вернуть, если память позволяет.

ARK>Если возврат через discriminated union, то забыть обработать не выйдет. Придется матчить тэг и смотреть, что вернулось.
Одними discriminated union вы не отделаетесь. Вам еще и union types нужно будет, иначе high-order functions очень неудобно реализуются. Везде, куда мы передали функцию, та функция может вернуть какой-нибудь код ошибки. Для каких-нибудь примитивов уровня OS это не важно. Там обычно все достаточно конкретно. А вот для функций общего назначения (iter, fold, map, filter, композиция функций и т.п.) подобное очень нужно.

Что характерно, после введения нормальных union types и вывода типов, вы получите те же самые checked exceptions, реализованные вручную

M>>Где здесь нелокальность???

ARK>При возврате кода ошибки мы всегда после вызова функции окажемся на следующей строке.
ARK>На исключениях мы этого гарантировать не можем — вот и нелокальность.

Так я же показал (развернул модель вычисления). На следующую строку управление и возвращается, только в состоянии abrupt completion. И обычно в языках есть нормальные способы таки узнать о наличии Abrupt completion и что-нибудь сделать. (те же try/catch/finally). Ну да, про наличие монады забывать нельзя. Но учитывая, что в любой строке может банальный stack overflow возникнуть, это вполне нормально. При повышенной безопасности (вроде проверки диапазона массивов во время выполнения) список различных проблем только увеличивается. Так что проще привыкнуть проверять необходимые гарантии освобождения ресурсов (благо в современных языках их не так много нужно контроллировать).

ARK>Но в языках общего назначения отказаться от таких штук совсем затруднительно.

Вот. С этим я тоже согласен. Но к многим штукам, которые ОП предлагает запретить, быстро и легко привыкаешь. Да, повсюду их пихать плохо. Но в удобных местах они экономят много кода и сил. Что такое "удобные" определяется многими факторами. Можно, например, стандартами кодирования и ревью контроллировать, что все в команде понимают код.

M>>Хуже того, return — это нелокальный переход (по той же причине)! return нужно запретить!


ARK>return вызывает переход на следующую строку после вызова. Вполне определенная точка.


После вызова где? Правильно, в какую-то неизвестную в данном месте строчку неизвестно кого.
И вы просто не встречались с нетривиальным return. Scala:
package com.pm.application.api

object Test extends App {
  private def xxx(a: => Int) {
    System.out.println("In XXXX")
    val aa = a
    System.out.println("AAA : " + aa)
  }

  private def bbb() : Int = {
    xxx({
      return 5
    })
    System.out.println("After XXX")
    6
  }
  
  System.out.println("BBB : " + bbb())
}

---- Output:
In XXXX
BBB : 5

Обратите внимание, return выходит совсем не туда, куда казалось бы. Да и простое "вычисление значения, переданного как call-by-name" является нелокальным переходом. Но это, конечно, экзотика (кстати, ловится обычным catch). Можно что-нибудь более каноничное и гораздо более предсказуемое (как минимум, я знаю по спецификации, как и что оно вычисляет). Java:
int calculateSomething() {
  int j = 5;
  for (int i = 0; i < 10; i++) {
    try {
      return i; /// Just a return!
    } finally {
      j = i;
      if (i != 7)
        continue;
      else
        break;
    }
  }

  return j+1;
}

Вот... Так что ваша теория работает. Но, наверное, не совсем так, как вы ожидали. Переход действительно выполняется на следующую строчку. Только вот в том же самом методе! И статус abrupt completion устанавливается в Return(value). В общем, никакой особой разницы с исключениями.


M>>"Нелокальный переход" — это другое. Это "у нас выполнялась функция, а теперь вдруг перестала и об этом мы вообще никак узнать не можем".

ARK>Не понял, чуть подробнее можете расписать?

Могу, конечно. Под "нелокальными переходами" я в первую очередь подразумеваю что-то вроде setjmp/longjmp. Это такое continuation на C. Совершенно небезопасное и неструктурное. Оно стек не раскручивает, а сносит и заменяет на новый. Это вот как раз нелокальный переход, а все остальное по сравнению с ним — детские игры.

Может быть, еще вариантами нелокального перехода являются lisp conditions и list continuations же, но я не знаю, как они работают, поэтому ничего определенного сказать не могу.

ARK>Кстати, педивикия так не считает: http://en.wikipedia.org/wiki/Control_flow#Structured_non-local_control_flow

Бывает. Но вот исключения, например, в зависимости от нижележащей модели вычислений, могут являться или не являться нелокальным переходом. В "монадной" модели — не являются. Поэтому я предпочитаю использовать ее. Может быть, conditions тоже сводятся к подобному. Например, там будет union различных установленных conditions в данный момент. И скорее это будет не локальный переход, а implicit context, например.
Re[6]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 25.03.13 19:10
Оценка:
Здравствуйте, maxkar, Вы писали:

Размеры постов превысили размер моего мозгового буфера. Поэтому только пара замечаний.

M>Сейчас это принято делать неблокирующими вызовами. interlocked increment или как оно там правильно в процессорах зовется. Фокус именно в том, что это выполняется на уровне процессора (с обеспечением всех безопасностей). И никаких явных синхронизаций, ожиданий и т.п.


Думаю, то, что выполняется на уровне процессора, может быть вынесено прямо в API. Как примитивная операция типа сложения.

M>Угу. Только вот ваш код в худшем случае требует примерно в три раза больше памяти, чем мой. А так — все зависит от ветвистости дерева.

M>На самом деле еще хуже. Мой код требует памяти O(depth), где depth — глубина дерева (на самом деле поиска, я в дочерние узлы не захожу, если текущий узел удовлетворяет результату). Результат "разбирается" на месте. Ваш код требует O(depth) на стеке и еще O(result) для возврата результата. Там что-то порядка двоечки в O(result) входит, потому что на самом верхнем уровне рекурсии мы можем копировать почти весь результат с одной из веток. Да и со временем выполнения у вас не все гладко .

А, ну с дочерними узлами я просто ошибся. Дополнительная память для результата — ну да, потому что мой код энергичный. Вам она тоже потребуется, если запросите всю последовательность. Хотя, если обрабатывать поэлементно, то наверное не понадобится. А со временем что не так (если отбросить мою ошибку с дочерними узлами)?

M>Пара проблем из обозначенных, конечно, решается. Нужно переписывать функцию на аккумулятор. Но тогда придется писать две функции, да и от лишних O(result) по памяти вы никуда не денетесь. Так что ваш код не эквивалентен моему. Он хуже по time/space complexity (преимущество у него будет только если стека больше, чем обычной памяти) и занимает больше кода.


Это, кстати, еще надо проверить — во что там переписываются ваши yield return.
А то может статься, что все хуже, чем вы думаете.

Кстати, мне думается, что ленивость можно сделать без yield return. Для программиста это будет выглядеть как мой код, но вместо List<TreeNode> будет нечто вроде LazyList<TreeNode>. Умный компилятор перепишет это в такой же ленивый генератор.

M>Кстати, если заменить весь ввод/вывод на асинхронный (т.е. приходят события о поступлении данных), то места исключениям сразу становится гораздо меньше. Они остаются только в парсинге пришедших данных. Всем остальным занимается большой и страшный Async-комбинатор. А все остальное — это уже ошибки в коде.


Не находите, что что-то здесь не так? С исключениями-то? Как асинхронный код — сразу старые добрые коды ошибок. По мне, это показывает несовершенство механизма.

M>Что характерно, после введения нормальных union types и вывода типов, вы получите те же самые checked exceptions, реализованные вручную


Но без прыжков неизвестно куда.

M>Так я же показал (развернул модель вычисления). На следующую строку управление и возвращается, только в состоянии abrupt completion.


У вас пример неверный, по-моему. После выхода из процедуры с вашим abrupt completion мы должны сделать GOTO на ближайший подходящий обработчик.
Концептуально возврат идет неизвестно куда — в случае успешного выполнения на следующую строку, в случае неуспешного — на какой-то обработчик фиг знает где выше.

ARK>>return вызывает переход на следующую строку после вызова. Вполне определенная точка.

M>После вызова где? Правильно, в какую-то неизвестную в данном месте строчку неизвестно кого.

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

M>И вы просто не встречались с нетривиальным return. Scala:

M>Обратите внимание, return выходит совсем не туда, куда казалось бы.

За такое надо вырывать с корнем руки. И такой return точно не нужен. Кстати, это то же самое, что и крышка в Smalltalk.

M>Можно что-нибудь более каноничное и гораздо более предсказуемое (как минимум, я знаю по спецификации, как и что оно вычисляет). Java:

M>Вот... Так что ваша теория работает. Но, наверное, не совсем так, как вы ожидали. Переход действительно выполняется на следующую строчку. Только вот в том же самом методе! И статус abrupt completion устанавливается в Return(value). В общем, никакой особой разницы с исключениями.

За такой двусмысленный код руки тоже надо того... На мой взгляд, здесь вместо знания спецификации должна быть ошибка компиляции. Наверняка в C# так и есть, хотя я не проверял. Впрочем, насколько я помню, Java позволяет всякую тупизну и типа такого:
  try
  {
    return 1;
  }
  finally
  {
    return 2;
  }

Это — чистой воды говнокод. И, заметьте, из-за goto-подобных операторов return, break и continue. Если бы их не было, то таких вопросов не было бы в принципе.


В целом из всех "прыгающих" операторов я пока не знаю, как отказаться от исключений (про это писал в одном из постов выше). Ну и от полиморфизма отказываться не стоит. Остальное, ИМХО, вполне можно упразднить с разной степенью успеха.
Re[7]: Нелокальные управляющие конструкции считать вредными
От: FR  
Дата: 26.03.13 05:42
Оценка:
Здравствуйте, AlexRK, Вы писали:

ARK>Кстати, мне думается, что ленивость можно сделать без yield return. Для программиста это будет выглядеть как мой код, но вместо List<TreeNode> будет нечто вроде LazyList<TreeNode>. Умный компилятор перепишет это в такой же ленивый генератор.


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

Но очень большой плюс yield кроме ленивости это возможность легко комбинировать
функции и использовать рекурсию, во многом аналогично ФВП в функциональных языках.
Например в питоне набор функций для итераторов http://docs.python.org/2/library/itertools.html
вполне сопоставим с базовым набором ФВП для списков http://caml.inria.fr/pub/docs/manual-ocaml/libref/List.html
в OCaml и также позволяет легко комбинировать и создавать функции более высокого порядка.
При этом выразительность получается вполне на уровне ФВП, как пример можно посмотреть
вот эту библиотечку для питона https://github.com/JulienPalard/Pipe .
Re[8]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 26.03.13 07:21
Оценка:
Здравствуйте, FR, Вы писали:

FR>Надо идти дальше из управляющих конструкций оставить только условный оператор и рекурсию.


Ну, это перебор. Я предлагаю минимизировать не число управляющих конструкций вообще, а число гото-подобных. Кстати, вот в Eiffel нет return, break и continue. А это все же не совсем маргинальный язык, хотя и не мейнстримовый.
Re[9]: Нелокальные управляющие конструкции считать вредными
От: FR  
Дата: 26.03.13 08:27
Оценка:
Здравствуйте, AlexRK, Вы писали:

ARK>Прошу прощения, не понял, можете пояснить? О чем должен догадаться компилятор?


О том где нужно использовать ленивость а где строгость, придется это указывать вручную.
В том же OCaml для этого есть функция force.

FR>>Но очень большой плюс yield кроме ленивости это возможность легко комбинировать

FR>>функции и использовать рекурсию, во многом аналогично ФВП в функциональных языках.
FR>>Например в питоне набор функций для итераторов http://docs.python.org/2/library/itertools.html

ARK>Посмотрел, но, по-моему, тут yield return опять же только ленивости везде. Все функции и без него переписываются 1-в-1 с обычными контейнерами. Я не прав?


Если не сохранять ленивость то да прав, но кому нужна квадратичная сложность вместо линейной?

Если ленивость сохранять то твой код сильно усложнится в сравнении с кодом использующим
yield. Например для того же OCaml есть библиотечка аналог itertools для питона
http://ocaml-batteries-team.github.com/batteries-included/hdoc/BatEnum.html если ее
использовать только комбинируя заданные ФВП все отлично, но если нужно писать свои базовые
функции то все становится грустно из-за отсутствия yield.

ARK>Посмотрел. Вроде тут везде просто применение уже готовых функций из itertools.


Я хотел продемонстрировать выразительность, тот же пример
euler2 = fib() | where(lambda x: x % 2 == 0)
               | take_while(lambda x: x < 4000000)
               | add

вполне нормально смотрится и на фоне хаскеля с окамлом.
Re[10]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 26.03.13 08:50
Оценка:
Здравствуйте, FR, Вы писали:

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

FR>В том же OCaml для этого есть функция force.

Эээ... а название типа ему в этом не поможет? Если функция возвращает List, то код энергичный, если LazyList — то делается генератор а-ля yield.
А на вызывающей стороне должно быть пофиг.

ARK>>Посмотрел, но, по-моему, тут yield return опять же только ленивости везде. Все функции и без него переписываются 1-в-1 с обычными контейнерами. Я не прав?

FR>Если не сохранять ленивость то да прав, но кому нужна квадратичная сложность вместо линейной?

А где квадратичная? По памяти в 2 раза больше, по времени то же самое. Нет?

FR>Если ленивость сохранять то твой код сильно усложнится в сравнении с кодом использующим

FR>yield.

Дык, мы же с этого начали — я сделал предположение, что с сферическим умным компилятором усложнения быть не должно (просто взять LazyList вместо List).
Re[7]: Нелокальные управляющие конструкции считать вредными
От: maxkar  
Дата: 26.03.13 14:40
Оценка:
Здравствуйте, AlexRK, Вы писали:

ARK>Здравствуйте, maxkar, Вы писали:


ARK>А со временем что не так (если отбросить мою ошибку с дочерними узлами)?

Сильно несбалансированное дерево. Допустим, даже двоичное. Левая ветка сразу является искомым узлом. Правая — продолжается перекошенное вправа дерево. А дальше все зависит от от реализации буфера. Если это что-то на базе одного массива, то вы будете на каждом шаге переписывать весь хвост в новый буфер (к одному элементу из левого поддерева). Итого общая сложность будет O(depth^2), где depth — глубина дерева. Наверное, могут спасти ropes (это "строки" с легкой конкатенацией).

ARK>Умный компилятор перепишет это в такой же ленивый генератор.

Ну да. FR уже заметил, что разницы между LazyList.add и yield return практически никакой (только в названии).

M>>Кстати, если заменить весь ввод/вывод на асинхронный (т.е. приходят события о поступлении данных), то места исключениям сразу становится гораздо меньше. Они остаются только в парсинге пришедших данных. Всем остальным занимается большой и страшный Async-комбинатор. А все остальное — это уже ошибки в коде.

ARK>Не находите, что что-то здесь не так? С исключениями-то? Как асинхронный код — сразу старые добрые коды ошибок. По мне, это показывает несовершенство механизма.

Да нет, все нормально. Как раз потому, что исключения нужно понимать в "монадном" смысле. Вот если бы исключения были нелокальным переходом, их можно было бы использовать. А так исключения всего лишь удобная обертка над кодами ошибок. Весь тот async — тоже обертка над похожими (асинхронными) кодами ошибок. Информация об ошибке является объектом (так же, как и исключение). Для большинства функций ошибки обрабатываются прозрачно (проходят на более высокий уровень), исключения всплывают подобным уровнем. У меня даже аналог catch есть (можно получить не только успешное значение, а результат с описанием ошибки или результатом). Вообще, async по сути образуют примитивный язык, поддерживающий только вызовы функций и литералы. Умел бы компилятор его правильно компилировать (не в "последовательной" монаде, а в "асинхронном выполнении"), был бы тоже имеративный код с исключениями


M>>Так я же показал (развернул модель вычисления). На следующую строку управление и возвращается, только в состоянии abrupt completion.

ARK>У вас пример неверный, по-моему. После выхода из процедуры с вашим abrupt completion мы должны сделать GOTO на ближайший подходящий обработчик.

Точно верный. Почему это мы должны сделать GOTO? У нас вызов завершился с abrupt completion. Если ничего специально не делать, все выражения в текущем методе тоже завершатся с abrupt completion. Затем аналогично завершатся методы еще на уровень выше и т.д. В конце концов обработчик будет найден. Весь этот сложный процесс примерно эквивалентен "переходу на подходящий обработчик" и приводит к такому же результату. try/finally — это тоже подходящий обработчик с последующим выбросом полученного ранее исключения. Какая-нибудь автоматика по вызову деструкторов в C++ — тоже подходящие обработчики (точно так же перебрасывающие исключения). Зато в подобной модели выполнения вся раскрутка стека детерминированна. Вроде бы во всех языках, которые я знаю, исключения как раз и работают по этой модели. Да, может быть, они описываются через "GOTO обработчик" и кучу описаний побочных эффектов от других инструкций (вроде вызова деструкторов при пролетающем мимо исключении). "Нелокальный переход" — это просто очень неудачное объяснение вполне удачной конструкции.


M>>Можно что-нибудь более каноничное и гораздо более предсказуемое (как минимум, я знаю по спецификации, как и что оно вычисляет). Java:

M>>Вот... Так что ваша теория работает. Но, наверное, не совсем так, как вы ожидали. Переход действительно выполняется на следующую строчку. Только вот в том же самом методе! И статус abrupt completion устанавливается в Return(value). В общем, никакой особой разницы с исключениями.

ARK>Впрочем, насколько я помню, Java позволяет всякую тупизну и типа такого:

ARK>
ARK>  try
ARK>  {
ARK>    return 1;
ARK>  }
ARK>  finally
ARK>  {
ARK>    return 2;
ARK>  }
ARK>

ARK>Это — чистой воды говнокод. И, заметьте, из-за goto-подобных операторов return, break и continue. Если бы их не было, то таких вопросов не было бы в принципе.

Позволяет. Побочные эфффекты удачно определенной монады. Как-то поведение все равно пришлось бы специфицировать (для обработки исключений, continue из catch и прочих радостей). Поэтому получилось то, что получилось. Да и нормально все. Ваш код — аналог следующего:
int retval;
{
  {
    retval = 1;
  }
  retval = 2;
}
return retval;

Это тот же код, переписанный в single return и на явных return state. Примерно такой же говнокод, но на Java это заметно лучше. От break/continue/return можно избавиться, это не слишком сложная задача. Но код усложнится (лишние проверки). Фактически, магию обработки (abrupt completion) вы выписываете вручную. И не факт, что потом в развернутом коде проблемы будет легко искать. А еще отказ от return'а приводит периодически к длинным-длинным лесенкам if'ов (вместо if(condition) return ... ; ), что явно не способствует читабельности кода.

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


А я бы, наоборот, повводил еще несколько моделей вычисления. Например, "реактивное программирование", когда присваивание значения переменной может приводить к изменению значений других выражений и вызову определенных методов. Тоже нелокальность (в духе полиморфизма). Так что я за сохранение и приумножение разнообразия различных моделей вычислений. Естественно, без лишнего фанатизма.
Re[12]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 26.03.13 19:05
Оценка:
Здравствуйте, FR, Вы писали:

ARK>>Дык, мы же с этого начали — я сделал предположение, что с сферическим умным компилятором усложнения быть не должно (просто взять LazyList вместо List).


FR>Не вижу как можно сделать такой компилятор.


Элементарно же. Вот это
IEnumerable<T> takewhile<T>(Predicate<T> predicate, IEnumerable<T> iterable)
{
  var result = new LazyList<T>();

  foreach (var x in iterable)
  {
    if (predicate(x))
      result.Add(x);
    else
      break;
  }

  return result;
}

автоматом переписывается в

IEnumerable<T> takewhile<T>(Predicate<T> predicate, IEnumerable<T> iterable)
{
  foreach (var x in iterable)
  {
    if (predicate(x))
      yield return x;
    else
      break;
  }
}



Может показаться, что это попахивает жульничеством. Но нет. Концептуально в первом варианте мы имеем один вход и один выход. А что там внизу — пофиг. В ассемблере вообще одни goto label, и что с того?

Более того — похоже, можно вообще любой метод помечать атрибутом [Lazy] и все! Безо всяких дополнительных управляющих конструкций и без потери читабельности.

А если пойти еще дальше — то ленивость можно сделать полностью автоматической оптимизацией. Я даже думаю, что программист далеко не всегда может сам определить, должен ли метод быть ленивым.
Где нужна вообще ленивость? (На полноту и истину не претендую.)
1) Бесконечные структуры данных. Оставим этот вариант безумным ученым. Если кому очень надо, можно сделать вручную.
2) Отложенные вычисления — надеемся, что (вдруг) вычислять вообще не придется. Это плюс. Минус — это ухудшает реактивность и предсказуемость работы системы в целом. Поэтому опять в топку, при сильной необходимости делаем вручную.
3) Когда не нужно хранить всю последовательность, а обрабатываем ее поэлементно. А вот тут компилятор может сам подставить ленивость (иногда ).
Одним словом, мне кажется, что ленивость и yield — это просто высокоуровневая оптимизация.

Еще раз основная мысль поста: на мой взгляд можно обойтись без yield return и не потерять в читабельности.
Re[8]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 26.03.13 19:20
Оценка:
Здравствуйте, maxkar, Вы писали:

ARK>>А со временем что не так (если отбросить мою ошибку с дочерними узлами)?

M>Сильно несбалансированное дерево. Допустим, даже двоичное. Левая ветка сразу является искомым узлом. Правая — продолжается перекошенное вправа дерево. А дальше все зависит от от реализации буфера. Если это что-то на базе одного массива, то вы будете на каждом шаге переписывать весь хвост в новый буфер (к одному элементу из левого поддерева). Итого общая сложность будет O(depth^2), где depth — глубина дерева. Наверное, могут спасти ropes (это "строки" с легкой конкатенацией).

Да нет же. Добавление элемента в массив выполняется за линейное время (амортизированное).
Так что все там нормально со временем. Да и с памятью тоже, ведь, как ни крути, O(2N)=O(N).

ARK>>Умный компилятор перепишет это в такой же ленивый генератор.

M>Ну да. FR уже заметил, что разницы между LazyList.add и yield return практически никакой (только в названии).

ИМХО, есть разница. Я там ответил — http://www.rsdn.ru/forum/philosophy/5114101
Автор: AlexRK
Дата: 26.03.13


M>>>Так я же показал (развернул модель вычисления). На следующую строку управление и возвращается, только в состоянии abrupt completion.

ARK>>У вас пример неверный, по-моему. После выхода из процедуры с вашим abrupt completion мы должны сделать GOTO на ближайший подходящий обработчик.
M>Точно верный. Почему это мы должны сделать GOTO? У нас вызов завершился с abrupt completion. Если ничего специально не делать, все выражения в текущем методе тоже завершатся с abrupt completion. Затем аналогично завершатся методы еще на уровень выше и т.д. В конце концов обработчик будет найден.

А, ну да. Но вообще это тонкий момент. Я, например, когда смотрю код, я не вижу, что функция "монадная". (Собственно, в этом и профит исключений.) А раз не вижу, значит концептуально "прыжок неизвестно куда" есть. То, что можно код переписать на abrupt completion — ничего не меняет, это всего лишь одна из возможных реализаций. Причем в реальности не использующаяся. Мы же не говорим, что if — это goto, несмотря на то, что реализовано оно именно так?

M>Это тот же код, переписанный в single return и на явных return state. Примерно такой же говнокод, но на Java это заметно лучше.


Я бы сказал, что на Java заметно хуже. Двусмысленность же. Недаром в C# это ошибка компиляции.

M>А я бы, наоборот, повводил еще несколько моделей вычисления. Например, "реактивное программирование", когда присваивание значения переменной может приводить к изменению значений других выражений и вызову определенных методов. Тоже нелокальность (в духе полиморфизма).


Да, тут недалеко в философии есть длинный разговор про реактивное программирование.

M>Так что я за сохранение и приумножение разнообразия различных моделей вычислений. Естественно, без лишнего фанатизма.


Дык — контроль нужен, вот в чем дело. Желательно формальный. Иначе кул ксакепы в порыве самореализации наплодят ужасов и это расползется по библиотекам. Вот, например: http://www.rsdn.ru/forum/java/5097238
Автор: AlexRK
Дата: 12.03.13
Re[14]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 27.03.13 12:37
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Во-первых, совершенно неочевидный для программиста. Ленивость — штука крайне важная. В частности, момент, когда вылетит NRE в случае передачи null в качестве аргумента, радикально зависит от ленивости либо энергичности.


Для автора метода все очевидно. А для пользователя разницы нет (это и сейчас так).

S>Во-вторых, вы взяли изолированный пример, который вам показался удобным. Отлично. Что будет делать ваш компилятор вот в этом примитивном случае:

S>Один вход, один выход, все дела.

Ошибка компиляции. LazyList должен быть только один. Но в принципе я согласен, что всякие подводные камни могут быть, возможно нужны и другие ограничения. Чего ради? Убрать goto-подобные скачки. Считаете это глупым непрактичным пуризмом? Ваше право. Я считаю иначе.

S>Ну-ну. Вы только что предлагали чёрную магию, построенную на сопоставлении специального типа результата, специального типа локальной переменной, и специальной стратегии использования. А тут вдруг рраз! и одним атрибутом всё щасте. Не верю.


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

S>Ну то есть вы предлагаете просто вернуться в С# 1.0. Там ленивость была именно такая — "при сильной необходимости делаем вручную". Не помните, почему мы оттуда ушли?


Не совсем просто, с дополнительной оптимизацией в тех местах, где обработка идет поэлементно.
И, кстати, я не считаю все эволюционные решения C# правильными.

ARK>>Еще раз основная мысль поста: на мой взгляд можно обойтись без yield return и не потерять в читабельности.

S>Смотря где. В императивных языках с энергичной семантикой — нет, нельзя.

Почему "накопление" результата в ленивом контейнере менее читабельно, чем yield return?

S>Если строить язык с нуля — то скорее имело бы смысл отказаться от коллекций, в которые в принципе можно делать add. Например, заменить их каналами. То есть любой add — это "yield return", который отдаёт управление тому, кто сейчас висит на get().


Можете короткий псевдокод привести? Звучит интересно, но как-то понимание ускользает.
Re[16]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 28.03.13 09:52
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Вот как раз для пользователя разница принципиальна. Это и сейчас так.

S>Банально потому, что его попытка сделать catch(NullReferenceException) внезапно не удастся.

Внезапно разницы между моим вариантом и использованием yield return в этом аспекте нет, поэтому к чему все эти претензии —

ARK>>Ошибка компиляции. LazyList должен быть только один. Но в принципе я согласен, что всякие подводные камни могут быть, возможно нужны и другие ограничения.

S>Ограничений будет вагон и маленькая тележка. Скажем, как будет себя вести ваш LazyList, если его передать аргументом в энергичный метод?

Да, тут какое-то тоже нужно ограничение. Возможно, лучше будет не LazyList, а языковая конструкция, но без выхода. Нечто типа "lazy result.Add(item)".

ARK>>Чего ради? Убрать goto-подобные скачки. Считаете это глупым непрактичным пуризмом? Ваше право. Я считаю иначе.

S>В yield return нет никаких "скачков".

yield return — это скачок управления в вызывающий код.

S>То, что вы предлагаете, либо убого по сравнению с yield return по функциональности, либо предлагает ровно то же. То есть в вашем коде нет абсолютно никаких преимуществ перед yield return.


Преимущество в одном выходе из метода. Собственно, с чего и начался весь разговор.

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

S>Отож. Вы слишком мало читаете Липперта.

Ай-яй-яй, обсуждаете мою квалификацию. Нехорошо-о-о.

ARK>>Не совсем просто, с дополнительной оптимизацией в тех местах, где обработка идет поэлементно.

S>Не вижу никакого простора для дополнительных оптимизаций.

Потенциальная оптимизация в том, что контейнер-результат не надо хранить целиком.

ARK>>Почему "накопление" результата в ленивом контейнере менее читабельно, чем yield return?

S>Потому, что непонятна семантика такого "накопления".

Что именно непонятно? Семантически мы имеем просто список (с несколько урезанным функционалом, конечно ) или какой-то другой специфический контейнер. Можем абстрагироваться от того, что на самом деле вместо списка генератор.
Отличие от yield в том, что управление у нас — концептуально — выходит из метода только в самом конце.

S>>>Если строить язык с нуля — то скорее имело бы смысл отказаться от коллекций, в которые в принципе можно делать add. Например, заменить их каналами. То есть любой add — это "yield return", который отдаёт управление тому, кто сейчас висит на get().


ARK>>Можете короткий псевдокод привести? Звучит интересно, но как-то понимание ускользает.

S>Очень просто всё.

Спасибо, понятно. Это то же самое, что каналы в Singularity или Go. Но это только усложняет код и ухудшает реактивность. С точки зрения читабельности плюсов не вижу. По моему мнению, чтобы эффективно писать программы с действительно массовым параллеллизмом (тысячи потоков), многопоточный код должен всячески мимикрировать под однопоточный. А у вас наоборот получается. Все ИМХО.
Re[18]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 28.03.13 11:30
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, AlexRK, Вы писали:


S>Т.е. lazy result.Add() — это не скачок? Ок, пишем свой awaiter — обёртку вокруг IEnumerator<T> и заменяем yield return на await enumerator.Yield(item). Что принципиально поменялось?


А что принципиально поменяется, если if заменить на goto label?

S>Не поверите, в итераторах тоже ровно один способ явно выйти — yield break. Замените "yield return" на "yield next", "yield break" на "yield exit" — получите желаемый синтаксис


Это уже второй способ. Первый — выход из метода в конце.
yield next — ну, может, вы и правы. Правда, нельзя посмотреть, например, все точки ленивого выхода, каким-нибудь find usage.

S>Даже при однократном переборе автоматическая оптимизация может стоить _очень_ дорого. В практике был случай, когда отказ от итератора и предварительное заполнение того самого контейнера ускоряло код (числомолотилка) в ~2-5 раз в зависимости от объёма кэша процессора. Если делать оптимизацию руками — никакой принципиальной разницы между yield return и lazy не будет.


Да? Может быть, спорить не буду. Тогда, получается, ленивость не нужна, недаром же эрланг энергичный.

S>Идее "допилим императивный язык для multicore-вычислений" фиг знает сколько лет и она всё никак не взлетит.


Я не совсем про это. Я скорее про много однопоточных приложений, каждое выполняется на своем ядре. (Ну это мечты, конечно.)

S>Для большинства прикладных задач за глаза хватает "распараллель мне вот этот цикл" и "Запусти вот этот код и продолжи по завершению". В шарпе, соответственно — PLinq и Task+await.


Пока да. Когда будут тысячи ядер, это работать не будет.
Re[17]: Нелокальные управляющие конструкции считать вредными
От: Sinclair Россия https://github.com/evilguest/
Дата: 28.03.13 13:55
Оценка:
Здравствуйте, AlexRK, Вы писали:

ARK>Внезапно разницы между моим вариантом и использованием yield return в этом аспекте нет, поэтому к чему все эти претензии —


Написать корректную реализацию yield return проще, т.к. там видна семантика.

ARK>Да, тут какое-то тоже нужно ограничение. Возможно, лучше будет не LazyList, а языковая конструкция, но без выхода. Нечто типа "lazy result.Add(item)".

ARK>yield return — это скачок управления в вызывающий код.
А ваш lazy result.Add(item) что, не даст скачка управления в вызывающий код? И каким же это образом?

ARK>Преимущество в одном выходе из метода. Собственно, с чего и начался весь разговор.

Я не вижу, где это у вас "один выход" из метода. Их у вас ровно столько же, сколько и в yield return.

ARK>Ай-яй-яй, обсуждаете мою квалификацию. Нехорошо-о-о.

Нет, я просто советую вам побольше читать Липперта.

ARK>Потенциальная оптимизация в том, что контейнер-результат не надо хранить целиком.

Это вы для энергичного случая? Непонятно, каким образом вы получите эту оптимизацию.

ARK>Что именно непонятно? Семантически мы имеем просто список (с несколько урезанным функционалом, конечно ) или какой-то другой специфический контейнер. Можем абстрагироваться от того, что на самом деле вместо списка генератор.

Нет, не можем. Он будет вести себя совершенно не так, как "обычный" контейнер.
Как вы думаете, куда денутся "лишние" элементы из вашего контейнера, когда его скормят на вход к Take(10)?
IEnumerable<T> Repeat10000times<T>(T item)
{
  for(int i=0; i<10000; i++)
      result.Add(item);
  return result;
}


ARK>Отличие от yield в том, что управление у нас — концептуально — выходит из метода только в самом конце.

Я так понимаю, что множественные return вы хотите тоже запретить?
Потому что иначе концептуально из iterator block мы выходим столько раз, сколько там yield break + закрывающая скобка.
Кстати, даже с вашим ограничением "1 lazylist на метод" и специальным синтаксисом, мы имеем чудеса в решете:
IEnumerable<T> split(bool takeOdds, IEnumerable<T> iterable)
{
  var odds = new List<T>();
  var evens = new LazyList<T>(); 
  int i = 0;
  foreach(var item in iterable)
    if (i++ % 2 == 1)
      odds.Add(item);
    else
      lazy evens.Add(item);

  return takeOdds? odds : evens;
}

Какая семантика у этого метода — ленивая или энергичная?
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[18]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 29.03.13 06:30
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>А ваш lazy result.Add(item) что, не даст скачка управления в вызывающий код? И каким же это образом?

S>Я не вижу, где это у вас "один выход" из метода. Их у вас ровно столько же, сколько и в yield return.

Концептуально — скачка нет и выход один.

ARK>>Потенциальная оптимизация в том, что контейнер-результат не надо хранить целиком.

S>Это вы для энергичного случая? Непонятно, каким образом вы получите эту оптимизацию.

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

S>Как вы думаете, куда денутся "лишние" элементы из вашего контейнера, когда его скормят на вход к Take(10)?


Ну, мысль такая, что в реальности они не будут получены вообще, а концептуально — просто отброшены.

ARK>>Отличие от yield в том, что управление у нас — концептуально — выходит из метода только в самом конце.

S>Я так понимаю, что множественные return вы хотите тоже запретить?


Ну да, первый пост про это в том числе.

S>Кстати, даже с вашим ограничением "1 lazylist на метод" и специальным синтаксисом, мы имеем чудеса в решете:

S>Какая семантика у этого метода — ленивая или энергичная?

Спасибо, хороший пример. Да, такой вариант не прокатит.

Этот ваш пример окончательно меня убедил, что свистоперделка под названием "yield return" не нужна. Если, конечно, не ставить целью засунуть в язык все, что может хоть когда-нибудь пригодиться.

Не спора ради, просто интересно: вы лично часто используете[-овали] yield return? В каких случаях?
Re[20]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 29.03.13 06:48
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Не поверите, с lazy result.Add всё точно так же — или return в середине, или return в конце метода.

S>Напомню, yield next — это просто переименованный yield return. Получается, семантика (поведение) не поменялось и проблема только в синтаксисе шарпа и отсутствии в студии пункта меню "найти все yield return в методе"

Ну да, слово lazy все портит. Как-то надо от него избавиться, если это возможно вообше.

ARK>>Да? Может быть, спорить не буду. Тогда, получается, ленивость не нужна, недаром же эрланг энергичный.


S>Не совсем, просто ленивость или должна быть явной, или весь кусок вычислений должен использовать ленивую модель, поддерживаемую инфраструктурой (как это происходит с await: добавил асинхронный метод — или делай все вызывающие тоже асинхронными, или превращай вызов в блокирующий).


Увы, плохо и первое, и второе.

S>Тоже не взлетит. (Источник и точные цифры не вспомню, но тут важно соотношение), 92% времени типового UI приложения — это ожидание ввода пользователя. Остальные 8% — дикое тормозилово из-за работы в UI-потоке. В случае с Metro UI время работы в UI-потоке уменьшается кардинально за счёт вытаскивания тормозов в await DoSomething(). Общее время выполнения при этом растёт (за счёт накладных расходов), зато отзывчивость UI практически идеальна.


Ну вот эти 8% и надо распараллелить.

S>Если мы забудем про UI и перейдём к собственно тяжёлой логике, то там 1 ядро на поток нам не особо поможет — ядра-то слабенькие, а императивный код распараллеливается плохо. Будем распараллеливать всё что можно — наткнёмся на закон Амдала.


Я думаю, будет нечто похожее на шейдеры — куча маленьких однопоточных программ.

S>Короче: для тяжёлых задач выбор практически не поменялся — или openMP и низкоуровневый хардкор на шейдерах, или специализированные скала с эрлангом, или умные балансировщики и куча мощных машин.


А с чего он должен был поменяться, если не существует коммерческих процессоров с миллионами ядер?

S>Для подавляющего большинства пользовательских задач ничего этого не надо, максимум выигрыша от 100500 процессоров получается на узкоспециализированных задачах — рендеринг, видео, и в принципе всё. Всё остальное упирается не в процессор, а в синхронизацию и получение данных из внешних устройств/потоков.

S>Если сможете придумать, накуда пользователю 1000 ядер — велкам. Только плиз, давайте без распознавания объектов, дополненной реальности и анализа генокода по фотографии Как показывает практика, подобные вещи рано или поздно выносятся в узкоспециализированные сопроцессоры, а собственно логика приложения как работала так и работает в одном потоке cpu.

640 кб хватит всем? Когда тактовую частоту наращивать больше не удастся, будут увеличивать число ядер. И что-то придумывать придется.
Re[19]: Нелокальные управляющие конструкции считать вредными
От: Sinclair Россия https://github.com/evilguest/
Дата: 29.03.13 08:32
Оценка:
Здравствуйте, AlexRK, Вы писали:
ARK>Концептуально — скачка нет и выход один.
Столько же, сколько в обычном iterator block. Минус один — выход через } у вас блокируется через "some execution paths do not return a value".

S>>Как вы думаете, куда денутся "лишние" элементы из вашего контейнера, когда его скормят на вход к Take(10)?

ARK>Ну, мысль такая, что в реальности они не будут получены вообще, а концептуально — просто отброшены.
За счёт какой волшебой магии они не будут получены вообще? Вот написан код; у него обычная императивная семантика. Чему будет соответствовать преждевременное прерывание итераций? В yield return всё понятно — мы вернули управление; гарантии того, что нам снова его отдадут, в общем случае нет.


ARK> Спасибо, хороший пример. Да, такой вариант не прокатит.

Отлично. Ещё немного, и вы изобретёте iterator block.

ARK>Этот ваш пример окончательно меня убедил, что свистоперделка под названием "yield return" не нужна. Если, конечно, не ставить целью засунуть в язык все, что может хоть когда-нибудь пригодиться.

Непонятно, как из того, что вы предложенная вами замена iterator block отвратительна, следует ущербность самого iterator block.

ARK>Не спора ради, просто интересно: вы лично часто используете[-овали] yield return? В каких случаях?

Регулярно. В случаях, когда мне надо было обеспечить прозрачное переписывание активного кода в реактивный.
Потому что альтернатива — ручное описание стейт-машины — многословна и затрудняет понимание сути вещей.
Вот — простая реализация концепции перебора дерева:
public static IEnumerable<T> IterateTree<T>(this T root, Function<T, IEnumerable<T>> children)
{
  yield return root;
  foreach(var child in children(root))
    yield return child.IterateTree(children);
}

Использование:
from Control c in this.IterateTree(control => control.Controls.Cast<Control>() 
 select c.ControlID)


А вот, например, простой и элегантный способ обеспечить последовательный workflow из нескольких диалогов в Silverlight, где весь UI асинхронен:
http://rsdn.ru/forum/dotnet/3147151.1
Автор: Sinclair
Дата: 22.10.08
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[20]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 29.03.13 10:03
Оценка:
Здравствуйте, Sinclair, Вы писали:

ARK>>Концептуально — скачка нет и выход один.

S>Столько же, сколько в обычном iterator block. Минус один — выход через } у вас блокируется через "some execution paths do not return a value".

Нет, выход один. Ах да, разумеется, это справедливо только для функций без побочных эффектов.

ARK>>Ну, мысль такая, что в реальности они не будут получены вообще, а концептуально — просто отброшены.

S>За счёт какой волшебой магии они не будут получены вообще? Вот написан код; у него обычная императивная семантика. Чему будет соответствовать преждевременное прерывание итераций? В yield return всё понятно — мы вернули управление; гарантии того, что нам снова его отдадут, в общем случае нет.

С точки зрения метода преждевременного прерывания итераций не будет. А в реальности конечно будет — в месте добавления элемента в список.

ARK>>Не спора ради, просто интересно: вы лично часто используете[-овали] yield return? В каких случаях?

S>Регулярно. В случаях, когда мне надо было обеспечить прозрачное переписывание активного кода в реактивный.

Понятно. (Лично я ни разу не использовал.)

S>А вот, например, простой и элегантный способ обеспечить последовательный workflow из нескольких диалогов в Silverlight, где весь UI асинхронен:

S>http://rsdn.ru/forum/dotnet/3147151.1
Автор: Sinclair
Дата: 22.10.08


Этот "элегантный способ для асинхронного UI", как вы любите выражаться, отвратителен.
Впрочем, программировать UI на Silverlight нормального способа не существует, разве что может с await, это я пока не пробовал.
Re[22]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 29.03.13 10:13
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Достаточно обрабатывать события асинхронно — например, вытащить обработку в фоновый поток, что не представляет особых проблем ещё с первого фреймворка.


А это что, не распараллелливание?

ARK>>Я думаю, будет нечто похожее на шейдеры — куча маленьких однопоточных программ.

S>Мошь шейдеров не в однопоточности, а в возможности параллельного выполнения последовательности операций над набором данных (aka SIMD). Часто такие вещи нужны в обычном бизнес-кода?

Я не говорю ни про какую мощь. Я привел шейдеры как пример "маленьких однопоточных программ".

S>>>Короче: для тяжёлых задач выбор практически не поменялся — или openMP и низкоуровневый хардкор на шейдерах, или специализированные скала с эрлангом, или умные балансировщики и куча мощных машин.


ARK>>А с чего он должен был поменяться, если не существует коммерческих процессоров с миллионами ядер?

S>Эмм... а как вы собираетесь эти ядра сопрягать с внешними устройствами и разруливать доступ к памяти? Шины — они не резиновые вообще-то

Я не собираюсь, это забота производителей процессоров. Помнится, на этом форуме Gaperton что-то писал про ниагару.

ARK>>640 кб хватит всем? Когда тактовую частоту наращивать больше не удастся, будут увеличивать число ядер. И что-то придумывать придется.


S>Ну блин! не нужно в 99% типовых сценариев массового распараллеливания. Смотрите закон Амдала — при сильной связности данных и при отсутствии однотипных вычислений распараллеливание становится неэффективным из-за затрат на синхронизацию. В оставшемся 1% случаев выигрыш достигается не банальным ключом компилятора "распараллель тут всё", а переводом всей модели вычислений на модель акторов или SIMD. Языки общего назначения для этого практически не подходят, специализированных решений — выше крыши. Как тут может кардинально поменяться ситуация?


Не нужно — пока, потому что нет нормальных решений. Кстати, я уже забыл, с чего все началось.

S>Возьмёт самый примитивный сценарий — читалку/блокнот. Один поток для рендера UI, один поток для обработки очереди сообщений, поток для всякой логики и поток для фонового чтения файла. Все потоки спят 90% времени, сам рендер уже отдан GPU через DirectWrite/WPF. Чем тут поможет миллион ядер? Возможностью захостить на одной ферме миллион терминальных сессий, из которых 990 000 будут отрубаться из-за ширины канала?


А давайте возьмем не самый примитивный сценарий. Компьютерная игра с огромным миром, живущим независимо от игрока, в котором в реальном времени разворачиваются события. Как тут поживают ваши "специализированные решения"?
Re[23]: Нелокальные управляющие конструкции считать вредными
От: Sinix  
Дата: 29.03.13 11:59
Оценка:
Здравствуйте, AlexRK, Вы писали:

ARK>Здравствуйте, Sinix, Вы писали:


S>>Достаточно обрабатывать события асинхронно — например, вытащить обработку в фоновый поток, что не представляет особых проблем ещё с первого фреймворка.

ARK>А это что, не распараллелливание?
Без обид, но у вас все баззворды собраны в одну кучу

На пальцах: асинхронность — асинхронное действие необязательно должно завершиться в момент перехода к следующей инструкции
var readTask = ReadTextAsync(someFile); // Запускаем асинхронное чтение

DoSomething(); // Метод выполняется синхронно

var text = await readTask; // Дожидаемся завершения чтения

// Продолжаем работу


Параллельность —
var x = Enumerable.Range(1, 1000*1000*1000).AsParallel().Sum

— означает "обработай набор данных, используй столько потоков, сколько удобно"

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

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

...

ARK>Не нужно — пока, потому что нет нормальных решений. Кстати, я уже забыл, с чего все началось.

ARK>По моему мнению, чтобы эффективно писать программы с действительно массовым параллеллизмом (тысячи потоков), многопоточный код должен всячески мимикрировать под однопоточный. А у вас наоборот получается. Все ИМХО.

Напомню, речь в топике шла про мейнстрим-языки а не про экзотику типа "у меня тут терма на пару экзафлопов и я не знаю чем её занять"

ARK>А давайте возьмем не самый примитивный сценарий. Компьютерная игра с огромным миром, живущим независимо от игрока, в котором в реальном времени разворачиваются события. Как тут поживают ваши "специализированные решения"?


Неплохо поживают. Например, 1 GeForce 8800 GT, ~16 000 агентов, 30 FPS вместе с визуализацией — http://www.flame.ac.uk/pubs/pdf/10.1.1.144.734.pdf

Ещё можно глянуть вот эти статьи — первое найденное, подробно не копался.
http://dl.acm.org/citation.cfm?id=1639831
http://jasss.soc.surrey.ac.uk/11/4/10.html

P.S. Мы по прежнему говорим про мейнстрим-языки и про вещи, реализуемые на типовых пользовательских компьютерах?
Re[24]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 29.03.13 12:19
Оценка:
Здравствуйте, Sinix, Вы писали:

S>>>Достаточно обрабатывать события асинхронно — например, вытащить обработку в фоновый поток, что не представляет особых проблем ещё с первого фреймворка.

ARK>>А это что, не распараллелливание?
S>Без обид, но у вас все баззворды собраны в одну кучу

Асинхронность по определению означает наличие второго потока исполнения (реального или виртуального — неважно).

S>Эти два понятия ортогональны.


В принципе да, но одно всегда влечет другое.

ARK>>Не нужно — пока, потому что нет нормальных решений. Кстати, я уже забыл, с чего все началось.

S>

ARK>>По моему мнению, чтобы эффективно писать программы с действительно массовым параллеллизмом (тысячи потоков), многопоточный код должен всячески мимикрировать под однопоточный. А у вас наоборот получается. Все ИМХО.


А, точно.

S>Напомню, речь в топике шла про мейнстрим-языки а не про экзотику типа "у меня тут терма на пару экзафлопов и я не знаю чем её занять"


Речь шла про языковые конструкции, а не мейнстрим-языки.

ARK>>А давайте возьмем не самый примитивный сценарий. Компьютерная игра с огромным миром, живущим независимо от игрока, в котором в реальном времени разворачиваются события. Как тут поживают ваши "специализированные решения"?


S>Неплохо поживают. Например, 1 GeForce 8800 GT, ~16 000 агентов, 30 FPS вместе с визуализацией — http://www.flame.ac.uk/pubs/pdf/10.1.1.144.734.pdf

S>Ещё можно глянуть вот эти статьи — первое найденное, подробно не копался.
S>http://dl.acm.org/citation.cfm?id=1639831
S>http://jasss.soc.surrey.ac.uk/11/4/10.html

Спасибо, гляну из дома. Хотя и так ясно, что там даже близко нет того, о чем я говорю.

S>P.S. Мы по прежнему говорим про мейнстрим-языки и про вещи, реализуемые на типовых пользовательских компьютерах?


Топик про управляющие конструкции в языках программирования. Не про мейнстрим-языки и не про вещи, реализуемые на типовых пользовательских компьютерах.
Re[26]: Нелокальные управляющие конструкции считать вредными
От: AlexRK  
Дата: 29.03.13 19:05
Оценка:
Здравствуйте, Sinix, Вы писали:

ARK>>Асинхронность по определению означает наличие второго потока исполнения (реального или виртуального — неважно).

S>Да ладно Реализации на локальной очереди, через петлю обработки сообщений, через подписку на IObservable, через таски, IO completition ports etc обходятся ровно одной абстракцией — continuation-passing. Зачем здесь вводить понятие "потока исполнения"?

Хм, вообще-то continuation-passing не имеет ничего общего с асинхронностью. Ортогональные, как вы говорите, понятия.
В википедии обнаружить чего-то асинхронное в соответствующей статье мне тоже не удалось: http://en.wikipedia.org/wiki/Continuation-passing_style

ARK>>В принципе да, но одно всегда влечет другое.

S>Нет. Асинхронность не означает параллельности и наоборот.

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

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


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

S>Аналогично, Parallel.For может развернуть цикл в несколько потоков, динамически раскидывать по потокам пачки элементов из исходной последовательности, запулить всё в один поток или замусорить очередь таскпула. С концептуальной точки зрения по прежнему ничего не поменяется — мы отдаём управление циклом на откуп рантайму в обмен на возможность загрузить все ядра CPU.


А кто сказал, что Parallel.For — "параллельный" метод?
Вот я напишу метод MamoyKlyanusYaParallelniy — вы его тоже будете считать параллельным?
Надо знать семантику метода. Если он выполняется в другом потоке — значит параллельный. Если в текущем — значит не параллельный. (А если он запускается в другом потоке управления, то скорее всего он будет и асинхронным, хотя возможен и бесполезный синхронный вариант )
"С концептуальной точки зрения", как вы говорите, ваш Parallel.For просто выполняет переданный ему делегат — и все.

ARK>>Топик про управляющие конструкции в языках программирования. Не про мейнстрим-языки и не про вещи, реализуемые на типовых пользовательских компьютерах.

S>Ок, тогда встречный вопрос:
S>кому интересны отдельные конструкции, которые малоприменимы в мейнстриме?

Ну мне, например, интересны. Но разговор, ИМХО, не совсем об этом. Применимы или нет в мейнстриме эти самые отдельные конструкции, которые мы обсуждаем — отдельный вопрос. Возможно, что и применимы. Мейнстрим все же имеет свойство меняться.

S>Повторюсь, для таких вещей лучше взять специализированный язык с подходящей моделью вычислений и не пытаться впихнуть всё в несчастный шарп


Шарп да, лопнет скоро. От всякого мусора типа yield return, dynamic и partial methods. С каждой версией язык становится все более эклектичен.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.