Будущее программирования (любопытная статья)

nikola1956

Будущее программирования
Paul Chiusano, 31.12.2011
Как будет выглядеть программирование через 10-20 лет? В канун нового года самое время пофилософствовать о будущем нашей индустрии. Мы находимся на пороге значительных изменений в деле написания программ, по сравнению с которыми нынешние, 2011 года, техники и идеи будут выглядеть примитивными. Изменения будут происходить в нескольких важных областях: инструментария и инфраструктуры, языков и систем типов, систем времени исполнения.
Прежде чем я начну, позвольте мне предупредить вас, что ниженаписанное стоит принимать с долей скептицизма: у меня нет хрустального шара для настоящих предсказаний. Существует множество причин, по которым несовершенные технологии могут доминировать в индустрии гораздо дольше, чем они того заслуживают. Я не буду особо распространяться на эту тему, хоть тема и определенно интересна. Вместо этого я хочу передать вам свое видение программирования будущего. Если бы не было накапливающихся сетевых эффектов, тянущихся за посредственными технологиями, используемыми сейчас, если программирование было бы изобретено заново, с чистого листа—каким бы я хотел его видеть?
Образующая идея этих изменений—избавление от случайной [инцидентной] структуры и от её сестры-близнеца, случайной сложности. Проявляющие себя во множестве форм, эти факторы составляют главное препятствие на пути конструирования более сложных программ; большинство основных изменений имеют своей целью развеять это препятствие. Новый мир программирования будет выглядеть по-другому: на порядки увеличатся повторное использование кода, легкость развертывания решений, эффективность приложений, произойдет множество других интересных штук.
Откуда начнем?
Первым существенным изменением будет уход от представления программ в виде текста, разделенного на файлы. Можно вас простить, если вы думаете, что это несущественная деталь. Кому какое дело, как программы представлены и в виде чего хранятся, так? Оказывается, этот крупный источник случайной сложности приводит к хлещущим через край последствиям вдоль всей цепи средств разработки, от захламления процессов мышления программиста, до сред разработки (мы обсудим их дальше) и систем сборки и развертывания. Как и в случае со многими другими вещами, здесь обсуждаемыми, размах, с которым здесь благодаря случайной сложности теряется продуктивность, остается за пределами радаров большинства программистов. Они невольно принимают эту потерю продуктивности как должное, вплоть до того, что находят «разумные» причины в оправдание своей зависимости от файлов и текста в программировании.
«А как насчет интенционального программирования?», можете вы спросить. Или сред разработки в духе тех, что у Smalltalk’а? Идея ухода от программ как текста, размазанного по файлам, не нова, но дьявол, как всегда, в деталях. Предыдущие попытки вырваться из лап этой случайной сложности закончились неудачно, в частности потому, что пытались заменить одну произвольную структуру другой. Например, интенциональное программирование отстаивало (хорошую, в общем-то) идею разделения представления кода и формата хранения кода, подсовывая формат (вроде XML) лишь с чуть меньшей случайной структурой. Преимущества, полученные от нового формата и структуры не были использованы образом, приводящим к заметной выгоде; при этом присутствовали вполне ощутимые затраты от потери сетевых эффектов, возникшие из-за того, что старые текстово-ориентированные утилиты были больше неприменимы. То же верно и для сред разработки Smalltalk’а. Также (что в некотором смысле иронично по отношению к Smalltalk, объектно-ориентированному языку) в основные предпосылки включен один из наиболее произвольных выборов из возможных, вечный генератор случайной сложности—решение о том, какой класс должен реализовывать конкретный метод.
Любая новая технология столкнется здесь с настоящим барьером. Она должна быть не просто лучше, чем замшелый status quo, она должна быть существенно лучше для того, чтобы преодолеть стоимость сетевых эффектов и перевесить за счет преимуществ затраты на переход с тех средств, которыми уже обладают существующие технологии. По моему мнению, ни одна из предложенных альтернатив тексту-в-файлах пока не приблизилась к взятию этого барьера.
Но нет никаких фундаментальных законов, которые бы запрещали это. Не стоит, исходя из того, что прошлые попытки провалились, заключать, что идея неосуществима. Просто попытки были недостаточно хороши. Первая крупная подвижка в осуществлении этой идеи: откажитесь от хранения программ в виде текста вовсе, даже от хранения в виде частично структурированного текстового формата вроде XML, тем более от бинарного частично структурированного формата, его все равно не избавить от присуствия случайной структуры. Избавьтесь также и от файлов. Храните вместо этого кодовую базу реляционно, как базу данных, с использованием инструмента вроде datalog для обновления и запросов. Этот язык обновлений и запросов должен быть тщательно продуман таким образом, чтобы широкомасштабные операции рефакторинга, в текущий момент сложно осуществимые или невозможные, были полностью (или частично, с помощью нескольких операций по трансформации кода в кодовой базе) автоматизируемы. В качестве простого примера избавимся от понятия о том, где функция или тип данных «расположены». Взамен каждая функция получает уникальный идентификатор (возможно, с помощью контентной адресации, дающей простую и автоматизированную форму дедупликации кода и отдельная таблица имен отображает эти идентификаторы в имена для нужд представления кода в некотором интересном человеку виде (вообще-то, ничто не мешает иметь несколько таких таблиц, так что разные программисты могут использовать разные имена и представления для одних и тех же действий). Это тривиальным образом разрешает производить рефакторинг «переименование» простым обновлением содержимого табличной ячейки.
Еще несколько мыслей о том, как это может работать. Во-первых, я не защищаю синтаксисdatalog, это меня не волнует. Ключевая возможность, которую дает datalog в дополнение к реляционной алгебре—возможность выражать транзитивное замыкание и взаимную рекурсию гарантированно завершимым образом. Эти черты дают возможность выразить многие обычные задачи в терминах трансформаций и запросов к кодовой базе. Например, вот гипотетический запрос для нахождения всех ссылок на данный идентификатор функции, fid. Не волнуйтесь, если синтаксис выглядит пугающе или бессмысленно. Ключевой момент в том, что это запрос всего лишь из нескольких строк кода, и он может быть использован заново, в том числе как строительный кирпичик для других запросов.
— propagate reference to containing apply
refs(Id) :- apps(Id, fid, _).
refs(Id) :- apps(Id, _, fid).
refs(Id) :- refs(X apps(Id,X,_).
refs(Id) :- refs(X apps(Id,_,X).
return(Id) :- lambdas(Id refs(Id).
return(Id) :- labdaBodies(Id, fid).
Нынешние интегрированные среды разработки, со всей поддержкой их со стороны громадных команд разработчиков и уймой кода специального назначения, позволяют весьма ограниченное подмножество операций по преобразованию и извлечению кода, все это работает медленно, плохо и с большими затратами ресурсов. Как следствие, существует целая субкультура программистов (включающая меня отвергающих IDE в пользу минималистских, не обладающих таким набором возможностей, но более надежных текстовых редакторов. Имеющиеся в современных самых совершенных IDE операции рефакторинга будут лишь началом возможностей будущих сред разработки.
Широкомасштабный рефакторинг—неизбежное следствие достижения той степени повторного использования кода, которая нужна для построения программ значительной сложности, не умирающих от [термодинамической] тепловой смерти. Время, затраченное на рефакторинг в этой новой модели занимает часы, а не недели и месяцы, и написание кода для трансформаций кодовой базы будет отдельным, но незаменимым умением, не переплетенным с обычным процессом программирования. Так что программисты не задумывают вначале рефакторинг (обычно достаточно простой, чтобы объяснить его другим программистам а потом начинают утомительный и полный ошибок процесс порчи текста руками, призванный реализовать рефакторинг. Вместо этого программист задумывает рефакторинг, затем придумывает программу для преобразования кода, имлементирующую рефакторинг, и применяет это преобразование к кодовой базе, весь процесс занимает несколько часов.
Концепция кода как базы данных имеет своим следствием обширнейшие упрощения и в других инструментах, в частности, для контроля версий. Проблема обработки слияний упорядоченных последовательностей символов, распределенных по файлам и директориям с произвольной структурой чрезвычайно сложна, соответственно, ПО для контроля версий содержит существенные объемы сложности. Эти трудности привели многих к неполноценным VCS моделям (Git, Mercurial где наборы изменений выстроены в линейный порядок и индивидуальные выборки [cherrypicking] просто не работают. В будущем, с кодовыми базами, мы увидим возрождение теории патчей Darcs, только в этот раз она будет работать так, как надо и будет реализовываться легко.
В связи с этим понятием кодовых баз мы получаем более тонкий и точный контроль зависимостей. Управление зависимостями—другая серьезная проблема разработки ПО, огромное препятствие для повторного использования кода, и, опять же, это обычно проходит мимо программистских радаров. Библиотека часто предоставляет какой-то новый интерфейс или абстракцию и дает несколько конкретизированных применений этой абстракции. Библиотека теперь зависит от того, доступны ли эти конкретные типы данных для компиляции. Использование этой библиотеки теперь означает включение всех её зависимостей, даже если вам нужен только один конкретный случай. Дополнительные траты от этого, объединенные с имеющейся сложностью сборок (опять же частично вызванной рассмотрением программ как текста) приводят к тому, что код используется повторно гораздо реже, чем это было бы возможно при другом раскладе.
В будущем мы будем в состоянии специфицировать код в зависимости от условий, не полагаясь на ad hoc технологии из 70-х вроде C-препроцессора. Язык запросов и обновлений вроде datalog хорошо здесь себя проявляет, и позволяет выражать вещи вроде таких: если конкретный тип X имеется в кодовой базе, определить некоторый набор функций и новых типов данных. Также, зависимости не будут присущи «библиотеке» или «модулю». Функция будет зависеть только от набора функций и типов, которые она использует, и кодовая база будет довольно гибкой штукой. Представьте: мы можем извлечь любой соединенный набор из функций и типов данных из кодовой базы, и это не требующее усилий автоматическое преобразование, поддерживаемое языков запросов. Здесь есть несколько неясных моментов вокруг версионирования (разрешимых, впрочем все они связаны с тем, как нам переопределить контроль версий так, чтобы задать частичный порядок над наборами изменений без ненужных зависимостей от представления программ как упорядоченных последовательностей символов.
Редактирование кода, среды разработки и системы типов
Редактирование кода будет производиться в структурных редакторах, которые будут совсем не похожи на современные IDE (те представляют из себя приукрашенные текстовые редакторы, ничего больше. Кстати, посредственные текстовые редакторы, если соскрести позолоту). В структурном редакторе программист будет создавать выражения, которые содержат незаполненные дырки. Важно, что эти структурные редакторы будут управляться типами, так что для каждой дырки программисту будет предложено множество значений, подходящее под ожидаемый тип, упорядоченное поудобнее. Редактор будет производить локальный поиск по программе для автодополнения сложносоставных выражений. Если вы когда-либо видели живую сессию программирования на Agda, вы знаете насколько мощной и продуктивной может быть эта идея. Да, текущий интерфейс написания программ на Agda родом из 70-х (самодельный мод для Emacs но мощь идеи управляемых типами редакторов видна. Ясно видно, что типы эффективны не только для предотвращения ошибок, но и для руководства и направления разработки. Это мощный усилитель наших маленьких программистских мозгов.
Сообразно этому, восход управляемой типами разработками в языках с настоящими, state of the art системами типов обозначит закат эпохи динамически типизированных языков (они же языки-с-одним-типом). Динамическая типизация будет считаться забавным, причудливым эволюционным тупиком в истории программирования. Эта точка зрения уже сейчас принята в некоторых программистских кругах, где принято считать, что большая часть защитников динамической типизации просто не знакома с управляемой типами разработкой в языке с настоящей системой типов и не знает, где находится передний край систем типов и теории языков программирования. В процессе дальнейшего развития области систем типов мы увидим, как последние преимущества динамических проверок тают в воздухе.
Связанное с этим изменение состоит в том, что типы станут еще более необходимы и системы типов обзаведутся новыми возможностями, чтобы лучше справляться с приданием данным структуры, отсутствие которой есть главный источник случайной структуры и сложности. Большая проблема больших кодовых баз—нормализация данных. Недостаток структурности в представлении данных ответственен за значительные количества склеивающего кода, соединяющего два куска кода таким образом, чтобы они могли общаться друг с другом. Большинство меинстримного программистского мира не осведомлено об этой проблеме, потому что повторное использование кода в большинстве императивных кодовых баз сильно ограничено (побочные эффекты сдерживают композицию).
В мире функционального программирования невообразимое количество кода используется повторно, хотя вместе с ним существует приличное количество кода, служащего для склейки. В качестве небольшого примера рассмотрим функцию f типа X -> Y -> Int, и значение xs
типа [(Y, X)] и предположим, что мы хотим применить f к списку xs. Опытный функциональный программист напишет map (uncurry f . swap) xs за три секунды, не задумавшись дважды. Но этот код почти полностью состоит из боилерплейта, его цель—убедить компилятор, что мы действительно хотим это сделать; это шум. Мы получаем повторно используемый код (императивный программист все еще писал бы for в миллионный раз вместо использования функций высшего порядка это так, но можно прийти к цели более чистой дорогой. Если этот код должен существовать (а я в этом не уверен: пояснения ниже я бы предпочел, добравшись до этого места в процессе написания, сказать моему редактору
map f over xs. Редактор найдет в программе необходимое для того, чтобы совместить типы, покажет мне программу для подтверждения если я это запрошу, и затем непокажет это подвыражение в главном окне редактора, возможно, просто показав вместо этого map f* xs, где * может быть развернута в полный текст.
Что даже лучше, мы, вероятно, сможем сделать то же с меньшим количеством кода для склейки. Такой код может быть удален двумя путями: первый способ, который я уже описал, в том, чтобы генерировать его автоматически, не показывая его до явного запроса. Второй способ—иметь типы(и системы типов выражающие больше структуры, так что нет случайной структуры, нуждающейся в стыковке. В качестве средств, делающих это возможным, мы увидим полиморфизм на строчных переменных, неупорядоченные типы и тому подобные вещи, а также различные механизмы систем типов, делающие эти средства удобными в использовании. Особенно интересной представляется мне явное разделение модели, лежащей в основе типа (она может быть упорядоченной, без случайной структуры) и представлений этого типа, которые могут содержать некоторую дополнительную структуру. Функции будут работать с моделью, задавая преобразования, из которых потом может быть восстановлено любое количество представлений. Компилятор и среда времени исполнения умны достаточно для выбора представлений времени исполнения, которые позволят сократить количество ненужных преобразований между представлениями.
Среды времени исполнения
Языки с нестрогой семантикой будут доминировать в новом мире благодаря увеличению повторно используемого кода и модульности, которая характерна для «нестрогости по умолчанию». Как я уже говорил в предыдущем посте, опциональная ленивость не справляется. Как и в случае с другими упомянутыми проблемами, проблемы со строгостью по умолчанию неочевидны для большинства программистов, включая тех, кто считает себя знакомым с областью функционального программирования. Проблемы, которые привносит строгость, становятся хорошо заметны только после существенного углубления в функциональный стиль разработки, в частности, после ознакомления с идеей библиотек комбинаторов и абстракций, необходимых для уничтожения дупликации кода [DRY] в этих библиотеках. Эти проблемы стали источником затруднений в библиотеке Scalaz, которая поддерживает функциональный стиль в Scala, строгом по умолчанию языке, также эти проблемы заметны в нашей рабочей кодовой базе на Scala.
Единственная существенная проблема с ленивостью—необходимость понимать, как используются память и стэк. Эта проблема пользуется популярностью среди людей, которые предпочитают находить своему нежеланию учить Haskell и ФП рациональные объяснения, но исследования и более умные стратегии вычисления в состоянии с ней справиться. Опять же, нет законов природы, утверждающих, что мы должны уступить и искать утешения в строгом порядке вычислений.
Привычная нормальная стратегия вычислений гарантированно завершима, если нормальная форма существует, но понимание того, как при этом используется память может быть проблематичным. Можно сделать лучше. Помимо статического анализа строгости, который покрывает только часть желаемых случаев, мы можем обратиться к дополнительным стратегиям вычисления, которые завершаются для тех же программ, что и нормальная стратегия и при этом могут обеспечивать дополнительную информацию о строгости. Из таких стратегий мне особенно интересна специализирующая стратегия с распространением строгости. Я опишу её подробнее отдельным постом, если вкратце, то в этой модели вычислений вызов функции подобен двум сообщающимся корутинам. При вызове функции вызываемая сторона начинает вычислять её тело, передавая контроль назад вызывающей стороне как только возникла необходимость в первом аргументе, и также отмечая, должен ли аргумент быть передан строго, опираясь на информацию, доступную во время исполнения. Последующие аргументы обрабатываются так же. В результате функции специализируются автоматически, по мере передачи аргументов, и мы не создаем отложенные вычисления, если они будут в строгом виде использованы вызывающей стороной. Это может быть эффективно реализовано с использованием двух стэков, возможны некоторые дополнительные оптимизации. Назначение такой стратегии—дополнить, а не заменить существующий статический анализатор строгости и передачи аргументов.
В общем, целью новых стратегий вычисления должна быть не столько эффективность (хоть это и хорошая цель сколько более простая модель использования памяти и стэка, так что эти вещи оказываются независящими от случайных деталей вроде того, как функция собрана или что компилятор решил заинлайнить (необходимость понимать эти вещи в Haskell ломает черную коробку абстракции функций, от которой зависит ФП и функции высшего порядка).
Среды времени исполнения и виртуальные машины будут все шире поддерживать подобные новые стратегии вычислений, и текущий выводок виртуальных машин «общего назначения», который ощутимо специализирован под строгие императивные языки, скорее всего отойдет на второй план.
Распространение кода и будущее web.
А что же с web, javascript, html 5 и остальным? Будем ли мы и дальше идти, прихрамывая, вместе с этими технологиями? Не думаю, что сильно удивлю кого-то, сказав, что написание приложений значительной сложности для web требует гораздо большей работы, чем требовалось бы при наличии действительно богатого клиентского языка. Снова, я не думаю, что многие web-программисты осознают, насколько сейчас всё плохо. По сравнению с меинстримными языками, такими как Java, C# или Python язык Javascript не так и плох; в некоторых отношениях он даже лучше. Но Javascript не выдерживает сравнения с языками, имеющими хорошую систему типов и настоящую поддержку ФП, вроде Haskell, Scala и тех языков будущего, которые придут им на смену.
Конечно, сторонники существующих web-технологий всегда найдут разумное объяснение существующему положению, указывая на то, как вещи улучшились по сравнению с тем, как оно было раньше. Это, возможно, верно. Но зачем останавливаться?
Предвижу будущее, в котором Javascript постепенно отходит на второй план, как уходят и специализированные плагины для браузеров вроде Flash, уступая место коду, написанному на произвольных, компилируемых языках, использующих для работы что-нибудь вроде NaCl. Это будет совмещено с кешированием подписанного кода и механизма отслеживания зависимостей, так что, например, станет возможным предоставить приложение, которое подгружает виртуальную машину Java и другие зависимости, если они еще не закешированы на вашем компьютере и подписи совпадают. Это изменяет способ мышления о программном обеспечении. ПО не всегда будет чем-то из категории «скачай это себе на компьютер и запусти». Напротив, ПО существует «само по себе», а вы его запускаете. Как деталь реализации процесса запуска вы можете позволить подгрузить дополнительный код, который нужен ему для работы.
Так закончится монополия HTML и Javascript на клиентской стороне, и большая часть затрат на переход к новым клиентским технологиям будет снята. Горстки низкоуровневых протоколов и стандартов будет достаточно, чтобы связать все вместе и поддерживать те хорошие вещи, которые существуют в web сегодня (позже я скажу об этом больше но не будет необходимости ни в чем настолько высокоуровневом, как задание конкретного языка с клиентской стороны для взаимодействия (будь то Javascript, Dart или какой угодно ещё язык) или клиентского языка для описания способа упорядочивания и отображения интерфейса (HTML + CSS). В будущем клиентский код будет писаться на каком угодно языка, компилируемом в нативный код по желанию.
Вместо, или вместе с поддержкой Native Client другим решением может стать некоторого сорта встроенная в браузер низкоуровневая виртуальная машина, сделанная людьми, понимающими, что они делают и знакомыми с передним краем разработок в области языков программирования и сред времени исполнения. Вероятно, это будут не те люди, которые разработали Dart, Go, Ceylon и другие языки, отстающие от переднего края на 30 лет или больше. Нам нужно что-то, что действительно может поддерживать работу языков будущего, а не перетасовывающие колоду на тонущем корабле нынешних меинстримных языков.
Что насчет предложения просто использовать Javascript как «язык ассемблера» для web, как конечную точку компиляции, и воздерживаться от прямого написания кода на нем? Это, очевидно, не лучшее решение. Компилирование неигрушечного языка программирования, вроде того же Haskell или что там придет ему на замену, в Dart или Javascript это, конечно, не то, что кто-то придумал бы, если бы ему дали чистый лист и попросили придумать все заново. Даже если получится это сделать без раздувания размеров кода с клиентской стороны сверх допустимого, остается неизбежный штраф эффективности от компиляции в язык, сравнимый с Javascript по производительности. Если вы задумаетесь об этом, нет реального повода оставаться на одной десятой или одной сотой от производительности, которой может достичь нативный код или виртуальная машина с хорошим JIT только из соображений легкости развертывания готовых решений. Этот компромисс себя не оправдывает, и с развитием NaCl или настоящей виртуальной машины в браузере мы будем лишены необходимости воспринимать этот компромисс всерьез.
Здесь есть один тонкий момент. Web обладает грандиозными сетевыми эффектами. Это составляющая её силы и полезности, нам не хотелось бы её терять. Нам все ещё нужны эквиваленты URL, но DOM нам не нужен. Что займет его место? Не нужен ли нам DOM для mashup’ов и поисковых движков? Нет. Кроме обхода DOM существуют другие пути, по которым программы могут получать информацию от приложений в web, и если вы задумаетесь об этом, то увидите, что в этом нет особенной сложности. Зачем царапать поверхность, когда вы можете использовать настояший API? Этот переход фактически уже происходит; большинство современных web-приложений предоставляют API в виде REST+JSON. Надо просто продолжать в этом направлении.
На что это может быть похоже? Во-первых, нам нужна стандартная система типов для web. Под этим я подразумеваю стандартный способ для приложений, которые живут по адресу URL предоставлять модуль с типами и функциями, которые они поддерживают. Лежащая в основе система типов должна быть достаточно выразительной, чтобы быть в состоянии выразить данные более богато, чем это сейчас позволяет JSON (который, кроме того, что динамически типизирован, не в состоянии эффективно выразить даже типы-суммы); такая система будет поддерживать алгебраические типы данных и некоторые из возможностей систем типов, упомянутые выше. С учетом этого появятся стандарты для некоторых сигнатур функций, со стандартными ролями для них. Так, например, googlebot вместо обхода DOM в поиске ссылок может вызвать функцию getAllLinks у приложения по данному URL, и получить готовый список URL. getAllLinks будет описано специальным стандартом, и новые стандарты могут появляться по мере появления новых режимов взаимодействия с web-сайтами. Так, будут почти универсальные стандарты (вроде getAllLinks) и более специализированные, рассчитанные под конкретные web-приложения (например Facebook предоставляет некоторые функции и типы данных, специфичные для Facebook, они навряд ли будут реализованы другими сайтами, хотя они и могут это осуществить).
Опять же, это уже начало происходить: существует множество ad hoc API и механизмов, по сути контролирующих то, как web-crawler’ы должны интерпретировать DOM.
При наличии стандартного способа программного взаимодействия с web-сайтами не будет больше нужды в DOM и мы сможем увидеть расцвет инноваций разнообразных технологий упорядочивания и изображения элементов.
В завершение: о становлении функционального программирования
Я уже намекал на это: функциональное программирование бесспорно победит. Не все разделяют мою точку зрения, однако многие из путей развития, о которых я говорил, возможны только в предположении, что мы работаем в пост-императивном, функциональном мире. ФП позволяет добиться поразительного прироста продуктивности благодаря существенному увеличению количества повторно используемого кода и облегчения рассуждений о поведении кода (не говоря про облегчение параллелизации). Индустрия постепенно начинает осознавать это, но то, что многие программисты по каким-то причинам не желают этому учиться, на самом деле неважно. Со временем давление отбора на конкурирующем рынке удалит менее продуктивные и эффективные техники, как сорняки на грядке, и это по большому счету будет означать конец императивного программирования как мы его знаем, за исключением нескольких нишевых областей. Независимо от начальных склонностей и предпочтений, программисты и компании которые желают быть конкурентноспособными, будут пользоваться услугами функционального программирования.
И относительно того, почему ФП не получило большего распространения уже сейчас: возможно, я напишу об этом подробнее в отдельном посте. Что я хочу сказать, так это то, что ФП сейчас достигло переломного момента, за которым открывается горизонт более широкого принятия и значимости. Здесь вступает в игру несколько факторов: технология компиляторов функциональных языков развилась достаточно, компьютеры стали мощнее, и, самое важное, сообщество ФП интенсивно открывает необходимые техники для организации больших программ с сохранением ссылочной прозрачности. Сообщество Haskell возглавляет эти перемены, так как Haskell, будучи единственным широко используемым языком с нестрогим порядком вычислений, не имеет другого выбора кроме как сохранять всепроникающую ссылочную прозрачность. Десять лет назад бытовало мнение, что выражение многих программ в чистом функциональном стиле требует новых исследований. Сейчас большинство этих техник уже известны, и выражение функциональности, обычно ассоциируемой с императивными программмами вполне возможно, а для опытного функционального программиста и нетрудно. Существуют еще интересные открытые вопросы относительного того, как представлять некоторые типы программ, но их количество стремительно уменьшается.
Тем не менее, я понимаю, почему многие заявляют, что ФП это слишком трудно, неестественно или неудобно. Подобно многим полезным дисциплинам, свободное владение ФП нелегко и требует усилий. Хотя после того, как этот барьер преодолен, написание программ в функциональном стиле вполне естественно и не требует особых затрат. (конечно, проектирование ПО продолжает быть тяжелой задачей, но проектирование функциональных моделей становится легче с практикой). Для стороннего наблюдателя техники ФП кажутся непонятными и неоправданно сложными. Для людей, свободно владеющих этими техниками в них нет ничего сложного или запутанного, зато выгода значительна. А тем, кто предпочитает критиковать ФП, я считаю, не помешает заиметь немного скромности. Пользуясь аналогией, человек без математического образования навряд ли будет чувствовать себя в состоянии опровергать и критиковать целые области математики («глупая задумка вещественный анализ» тем не менее программисты с очень приблизительным пониманием того, про что такое ФП находят в себе силы регулярно и громко критиковать его.
Я понимаю, почему некоторые разочарованы существующими ресурсами для изучения предмета. Но неверно отвергать целую дисциплину по этой причине, или оглядываясь на личности тех людей (включая меня! которые считают, что ФП более продуктивно. Это обективный параметр: или ФП стоит изучения благодаря продуктивности и другим выгодам, или нет. Что касается меня, то я один из соавторов книги по ФП, которая, надеюсь, будет полезна заинтересованным в изучении предмета. Со становлением техник ФП я надеюсь видеть все больше ресурсов, подобных этому, вплоть до состояния, когда свободное владение и значительная выгода в итоге будет чем-то, чего любой мотивированный программист сможет достичь, без необходимости сталкиваться с некоторыми разочаровывающими препятствиями, которые поджидают его на этом пути сегодня.
Будет ли программирование в будущем выглядеть похожим на то, что я описал? Может быть, а может и нет. Но я надеюсь, что будет.
http://ajc.su/koding/budushhee-programmirovaniya/

Maurog

подтверждаю! любопытная! :)

Hastya

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

durka82

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

al70

Еще любопытнее было бы почитать какую-нибудь подобную статью, только 1992 г. выпуска. Всегда нравилось сравнивать предсказания экспертов с тем, как на самом деле вышло.

Marinavo_0507

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

salamander

1992, но про ОС и процессоры:

In the meantime, RISC chips happened, and some of them are running at over 100 MIPS. Speeds of 200 MIPS and more are likely in the coming years. These things are not going to suddenly vanish. What is going to happen is that they will gradually take over from the 80x86 line. They will run old MS-DOS programs by interpreting the 80386 in software.


Of course 5 years from now that will be different, but 5 years from now everyone will be running free GNU on their 200 MIPS, 64M SPARCstation-5.

apl13

64M-то будет достаточно каждому?

6yrop

Редактирование кода будет производиться в структурных редакторах, которые будут совсем не похожи на современные IDE (те представляют из себя приукрашенные текстовые редакторы, ничего больше. Кстати, посредственные текстовые редакторы, если соскрести позолоту). В структурном редакторе программист будет создавать выражения, которые содержат незаполненные дырки. Важно, что эти структурные редакторы будут управляться типами, так что для каждой дырки программисту будет предложено множество значений, подходящее под ожидаемый тип, упорядоченное поудобнее. Редактор будет производить локальный поиск по программе для автодополнения сложносоставных выражений. Если вы когда-либо видели живую сессию программирования на Agda, вы знаете насколько мощной и продуктивной может быть эта идея. Да, текущий интерфейс написания программ на Agda родом из 70-х (самодельный мод для Emacs но мощь идеи управляемых типами редакторов видна. Ясно видно, что типы эффективны не только для предотвращения ошибок, но и для руководства и направления разработки. Это мощный усилитель наших маленьких программистских мозгов.
алё, человек не видевший ReSharper-а :confused:

6yrop

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

karkar

Решарпер позволяет оставить в выражении дырку, для которой он выведет тип отсутствующей части и предложит подходящее по типу выражение?
Agda (from version 2.2.6) has a command, ‘Auto’, which searches for type inhabitants. It can be used as an aid when interactively constructing terms in Agda. In a system with dependent types it can be meaningful to use such a tool for finding fragments of, not only proofs, but also programs. For instance, giving the type signature of the map function over vectors, you will get the desired function as the first solution.

6yrop

Smart Code Complition, не то?

6yrop

... обозначит закат эпохи динамически типизированных языков (они же языки-с-одним-типом). Динамическая типизация будет считаться забавным, причудливым эволюционным тупиком в истории программирования. Эта точка зрения уже сейчас принята в некоторых программистских кругах ...
интересны комментарии :smirk:

nikola1956

Вместо этого программист задумывает рефакторинг, затем придумывает программу для преобразования кода, имлементирующую рефакторинг, и применяет это преобразование к кодовой базе, весь процесс занимает несколько часов.
============================================
я это делаю уже сейчас
Очень интересно, какой язык Вы используете для написания программ для преобразования кода? И собственно на каком языке написан этот код?
Можно ли по-быстрому написать свой DSL (напр., на Antlr или с помощью парсер комбинаторов на Scala или Haskell) для программирования рефракторингов Java/Scala - кода?
Какие результаты такого подхода? Упростилась и ускорилась ли разработка?

karkar

Smart Code Complition
Да, очень близко.

apl13

предложит подходящее по типу выражение?
π-теорема, ня? :ooo:

elenangel

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

apl13

In Western world television is something from the future.
In Soviet Russia FUTURE is something from TELEVISION.

bleyman

Редактирование кода будет производиться в структурных редакторах, которые будут совсем не похожи на современные IDE (те представляют из себя приукрашенные текстовые редакторы, ничего больше.
Присоединяюсь к Шурику: перед нами явно любопытный экземпляр программиста, считающего Vim "state of the art" IDE, и делящегося своими революционными идеями по улучшению "state of the art".
Языки с нестрогой семантикой будут доминировать в новом мире благодаря увеличению повторно используемого кода и модульности, которая характерна для «нестрогости по умолчанию».
Перестал читать после этого. Simon "Python" Jones сказал что "следующий Хаскель" будет strict, потому что хотя ленивость по дефолту заставила их "быть честными" и придумать много полезных штук, она приводит к жуткому геморрою на практике.
Я как-то SPJ больше доверяю чем какому-то хую с горы, который в своём предыдущем посте на который он тут ссылается продемонстрировал что он не понимает что ленивость функций никого не ебёт, это ленивые структуры данных интересны. (если чо, оригинал тут: http://pchiusano.blogspot.com/2011/12/future-of-programming....).

karkar

Хаскельная ленивость уже набрала большое количество фанатов, это направление будет жить еще долго. "Жуткий геморрой" за последние 20 лет более-менее победили. Противопоставлять ленивые структуры данных функциям нет смысла, эти структуры и есть результат работы ленивых функций, они нераздельны.
С другой стороны, мне кажется, что таки можно поиметь плюшки ленивости, не делая ленивую семантику дефолтной. Я тут недавно начал писать на D, там все алгоритмы стандартной библиотеки основаны на понятии range - развитии итераторов из STL, и по сути обработка данных там ленивая, что дает ту модульность, о которой говорили большевики. Но при этом за счет богатой компайл-тайм логики они много умнее, чем ленивые списки. Например, получив range в качестве аргумента, можно узнать, конечен он или бесконечен, дает ли random access и т.п. и применить разную логику по его обработке, причем выбор произойдет еще в момент компиляции.

bleyman

Противопоставлять ленивые структуры данных функциям нет смысла, эти структуры и есть результат работы ленивых функций, они нераздельны.
Ты что, абстракция и её имплементация очень даже разделимы, то, что хаскель компилируется в машинные коды, вовсе не означает, что программистов на хаскеле волнуют определённые проблемы x86 архитектуры.
Точно так же там чувак говорит что вот, представьте себе произвольную функцию ленивую в таком-то аргументе, а теперь мы хотим этот аргумент передать в другую функцию, омг ленивость обязана расползаться по всем функциям, так что она нужна по дефолту. Это становится очевидной неправдой если мы говорим только про ленивые структуры данных, потому что тогда ленивость должна расползтись только по имплементациям всяких map/filter/foldr и вовсе не нужна по дефолту во всех остальных функциях.
С другой стороны, мне кажется, что таки можно поиметь плюшки ленивости, не делая ленивую семантику дефолтной. Я тут недавно начал писать на D, там все алгоритмы стандартной библиотеки основаны на понятии range - развитии итераторов из STL, и по сути обработка данных там ленивая, что дает ту модульность, о которой говорили большевики.
Пока большевики говорили о светлом будущем, загнивающие империалисты использовали нормальные итераторы с середины 90х в Питоне и c 2005 (если не раньше) в C#. Исчо раз повторяю: stl больше 20 лет уже, это не state of the art никак, когда внезапно выясняется, что под расовым превосходством функционального программирования имелось в виду превосходство над stl, мне хочется закатить глаза и жеманно вздохнуть.
Я, кстати, не уверен что все эти дополнительные фишки D нужны вообще, там же тот же Степанов руку приложил... Типа, по моему личному опыту нужен один вид итератора: input iterator (aka forward, one-shot плюс разделение на iterable and iterator. Всё остальное — типичная архитектурная астронавтика, ох, давайте напишем на бумажке все возможные атрибуты итераторов, получим матрицу вариантов, и вместо того, чтобы посмотреть нужны ли они все кому-нибудь вообще, заимплементим все, и всё остальное ужасно криво чтобы поддержать их все! Раз есть инпут итераторы, то должны быть и аутпут, о, отлично, теперь можно забить на нормальный чейнинг (то есть генераторы и yield keyword) и заимплеменить все библиотечные функции чтобы они требовали output iterator куда писать результат, и пофиг что они в результате не composable получились!

karkar

когда внезапно выясняется, что под расовым превосходством функционального программирования имелось в виду превосходство над stl
Не-не-не, этого тут никто не говорил, тебя сочетание этих слов в одном посте куда-то не туда направило. И STL никто state of the art не называл тоже.
Достаточно ли одних итераторов для счастья - не уверен. Апологеты хаскеля, когда говорят о приносимой ленивостью модульности, приводят пример, когда один модуль умеет геренить дерево состояний, а второй по нему ходит, при этом первому не нужно генерить все дерево, а второму не нужно подстраиваться под порядок генерации, он сам его направляет.
В D я как-то не приметил требований output iterator, наоборот там на chain/map/filter/reduce все неплохо так композится, вполне похоже на то, что привык видеть в ФЯ. Разнообразие атрибутов range'й там нисколько не мешает, зато способствует эффективности. Скажем что-то типа
(! 5) . reverse . take 40 . reverse $ map show [1..1000000]
там выполнится за О(1 т.к. map, take и т.п. сохраняют свойства типа "имеет конечную длину" и "дает доступ по индексу". Обычные итераторы и списки так не умеют.

bleyman

> один модуль умеет геренить дерево состояний, а второй по нему ходит, при этом первому не нужно генерить все дерево, а второму не нужно подстраиваться под порядок генерации, он сам его направляет.
Ну конечно, тогда делаешь ленивое дерево и так далее... Я к тому, что когда ты явно думаешь про ленивость структуры данных вместо ленивости произвольных функций, у тебя нигде не появляется этот FUD про "optional laziness doesn't cut it", всё в порядке, никуда ленивость не пытается расползаться.
Про итераторы я заговорил потому что ты заговорил про ranges in D, я типа не знаю как именно они выглядят и похожи ли на boost::range 2.0 и адапторы, но те сосут например, потому что предоставляют кучу ненужных концепций и не предоставляют нужных, типа нормальных генераторов. Тогда как если не выпендриваться и сделать нормальные input iterators, этого хватает на 99.9% ситуаций где ты хочешь использовать range.
> там выполнится за О(1 т.к. map, take и т.п. сохраняют свойства типа "имеет конечную длину" и "дает доступ по индексу".
А если я на каком-то этапе случайно передам трансформацию которая строго последовательная, то всё скомпилится отлично, но с замедлением в 50 раз (или вообще со сменой класса сложности)?

apl13

А если я на каком-то этапе случайно передам трансформацию которая строго последовательная
Ты не должен хотеть такие трансформации.

Dasar

А если я на каком-то этапе случайно передам трансформацию которая строго последовательная, то всё скомпилится отлично, но с замедлением в 50 раз (или вообще со сменой класса сложности)?
Компилится-то оно в любом случае должно.
Речь может идти только о том, что до запуска (с помощью статического анализа) должна выдаваться оценка производительности

karkar

А если я на каком-то этапе случайно передам трансформацию которая строго последовательная, то всё скомпилится отлично, но с замедлением в 50 раз (или вообще со сменой класса сложности)?
Зависит от конкретной трансформации, сохраняет ли она оператор произвольного доступа и как именно его реализует. Может скомпилиться с замедлением, а может и не скомпилиться, особенно если явно в своем коде потребовать в компайл-тайме выполнение условия isRandomAccessRange. Если автор трансформации следовал общему стилю, то скорее всего свойство RandomAccess будет потеряно и код в том же виде не скомпилится, зато будет доступен другой способ получить n-ный элемент, явно неконстантной сложности.

pilot

интересны комментарии :smirk:
Не вижу в статье чего-то, заслуживающего комментариев.

Papazyan

когда внезапно выясняется, что под расовым превосходством функционального программирования имелось в виду превосходство над stl, мне хочется закатить глаза и жеманно вздохнуть.
Может ты не будешь рассуждать о преимуществах ФП, если у тебя такие извращенные представления о них?

apl13

Типа, по моему личному опыту нужен один вид итератора: input iterator (aka forward, one-shot)
А расскажи, кстати, как при помощи одного forward iteratorа найти хотя бы значение в массиве.
Ну то есть тебе пять минут назад понадобился массив, причем весь, ты его весь получил, и с тех пор он лежит, и память была не нужна с тех пор. И тут нужно найти в массиве наименьший индекс, для которого верен предикат. Предикат пусть чистый.

bleyman

Я раньше сказал же:
> Типа, по моему личному опыту нужен один вид итератора: input iterator (aka forward, one-shot плюс разделение на iterable and iterator.
Причём iterable имплиситно кастится в итератор. Как это сделано в Sea Octothorpe and Python.
Типа, хочешь найти минимальный элемент в массиве — передавай в функцию min массив. Хочешь найти минимальное значение в отфильтрованом и промапленом массиве один раз — можешь туда же передать итератор, а если несколько раз, то зафорсь итератор чтобы получить strict массив и передавай куда хочешь.
Тут могут возникнуть всякие мысли насчёт того, что в некоторых же случаях можно сделать специальные итераторы которые автоматически позволяют делать tee (то есть 'forward' instead of 'input' а то и вовсе random access, но это всё архитектурная астронавтика, ибо в 99% случаев не нужно, но доставляет дичайший геморрой в том числе и когда нужно.
Кстати тут мне недавно один знакомый на Скалу жаловался, они оказывается решили что самые умные, и что проблема STL-подхода только в том, что там у них недостаточно МАГЕИ вокруг этого всего обёрнуто. Ну и щедрой рукой добавили магеи. Так что теперь в Скале эквивалент

def masked_list(iterable):
   return ', '.join(map(lambda s: '*' * len(s iterable

возвращает вовсе не '*, *, *', а совсем даже и просто '*', если сделать
masked_list(set('a', 'b', 'c'

Потому что map например возвращает set если ей передать set.
Злонравия достойные плоды.

karkar

Кажися, создатель clojure Рич Хики одно время выступал на партсобраниях с идеями о том, что подобные input iterator'ы, которые можно проходить только последовательно, - это прошлый век и атавизм, ибо они мешают параллельной обработке данных там, где она так возможна. Нужны более другии базовые интерфейсы. Кажется, предлагал вместо них деревья, чего с лиспера взять.

apl13

Типа, хочешь найти минимальный элемент в массиве — передавай в функцию min массив.
Епрст. А ты раньше говорил, отличаешь ли ты минимальный элемент от минимального индекса?
Или ты контейнер с рандомным доступом предлагаешь итерировать последовательно, СНИЗУ ВВЕРХ! СНИЗУ ВВЕРХ! :ooo:

bleyman

Я нифига не понял твой вопрос. Минимальный индекс в массиве это ноль. Чтобы найти минимальный элемент в массиве (и его индекс, если интересно да, я, как ни странно, предлагаю итерировать последовательно. Вместо того, чтобы итерировать псевдослучайно, пользуясь возможностью рандомного доступа.
@deemon: я, пожалуй, процитирую Simon "Die Grosse Python" Jones:
All of that said however – it’s not to say that purely functional programming means parallelism without tears. You can’t just take a random functional program and feed it into a compiler and expect it to run in parallel. In fact it’s jolly difficult! Twenty years ago we thought we could, but it turned out to be very hard to do that for completely different reasons to side effects: rather to do with granularity. It’s very, very easy to get lots of very, very tiny parallel things to do, but then the overheads of doing all of those tiny little things overwhelm the benefits of going parallel.
Вот если сейчас SPJ скажет что таки смог допилить nested data parallelism или что он там собирался, и для некоторых программ ему удаётся их параллелить автоматически, я послушаю. А когда Рич Хикки говорит что сам не пробовал, но ему кажется что может быть sufficiently clever compiler сможет параллелить программы автоматически если использовать структуры данных которые не мешают параллелизации, то есть типа что надо начать использовать такие структуры данных, а остальное приложится как бы само собой, то извини, я остаюсь скептичен.

karkar

А речь совсем необязательно об автоматическом распараллеливании. Речь скорее о стиле программирования - если в языке идиоматично все делать на списках или последовательных итераторах, то параллелизм организовать сложнее. Вон в С#/C++/D я беру массив, заменяю в его обработке обычный for на parallel for, и оно внезапно работает параллельно (конечно, тут самому приходится следить за тем, чтобы код внутри цикла не делал глупостей). Аналогично с map и reduce. Со списками и итераторами это уже сложнее и не так естественно.

yroslavasako

reduce
В скала как раз reduce и fold разные понятия. Одно гарантировано последовательное, другое - произвольное.

bleyman

Ты _который_ for заменяешь на параллельный, если у тебя их много?
Я вот к чему: мне кажется что ты всё ещё думаешь в терминах автоматической или полуавтоматической локальной оптимизации, когда ты можешь взять и распараллелить _каждый_ комбинатор. Так что у тебя есть filter который в 16 тредов фильтрует массив, а потом map который в _другие_ 16 тредов применяет к результатам какую-то функцию, а потом специально обученный моноидный reduce который в третьи шестнадцать тредов редьюсит результаты.
Заранее понятно, что это будет глючить и тормозить, как указал в своей мудрости SPJ, причём неважно, это тупой компилятор такую фигню делает, или программист может не задумавшись спороть.
А если делать по уму, то есть сначала прикинуть где у тебя будет происходить основная работа которую имеет смысл параллелить, то наверное получится что-то вроде: пройтись по входному списку отдавая его куски 16 тредам, каждый из которых пропускает данные сквозь весь пайплайн, потом аккуратно смержить результаты. Или каждый тред сам просит очередной chunk когда закончил с предыдущим, чтобы наиболее эффективно распределять нагрузку.
При этом никого уже не волнует что исходный список приходится обходить последовательно, это не то, что жрёт цпу. А если есть разделение на iterators and iterables, то даже и это необязательно.

karkar

Ты _который_ for заменяешь на параллельный, если у тебя их много?
Только тот, который сам сочту подходящим для этого. Обычно это внешний, когда их много. Автоматически делать все или многие комбинаторы параллельными я не предлагаю, это тебе напрасно кажется.
Кстати, известные мне реализации в трех упомянутых языках не будут делать по 16 потоков на каждую операцию, они используют тредпул и очередь заданий.
 
При этом никого уже не волнует что исходный список приходится обходить последовательно, это не то, что жрёт цпу.

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

bleyman

Только тот, который сам сочту подходящим для этого. Обычно это внешний, когда их много.
Вот именно! Только ты имел в виду внутренний? Как он будет у тебя фильтры параллелизировать, если он внешний? А вот когда он внутренний, он работает с итераблем, а не с итератором, причём с конкретным типом итерабля который позволяет рандом аксесс а так же делать слайсы, из которых получаются input iterators.
Кстати, известные мне реализации в трех упомянутых языках не будут делать по 16 потоков на каждую операцию, они используют тредпул и очередь заданий.
Всё равно будет тормозить же. Если ты ставишь в очередь заданий задания вида "достать очередной элемент, если он не меньше десяти то отправить дальше", то вся херня займёт приблизительно в десять тысяч раз больше времени, чем сама проверка. Я серьёзно про десять тысяч раз, кстати.
Если исходные данные передаются через инпут итератор, раздача данных потокам вероятно потребует копирования данных, что в случае, когда данных много, нежелательно. С RandomAccessRange это не требуется.

Ты же не будешь делать value array для элементов большого размера? Так что либо копирование дешёвое, либо копируется поинтер.
Не, я понимаю что бывают интересные ситуации, когда ты например пишешь быстрое параллельное умножение матриц. Но в таких случаях ты заранее знаешь тип своего итерабля (НЕ итератора работаешь с итераблями и всё такое.
Приведи пример, короче, когда тебе нужен именно random access iterator (не iterable).

karkar

Вот именно! Только ты имел в виду внутренний?
Нет, я имел в виду внешний. Иначе будет очень fine-grain, о чем говорил господин ПЖ.
Всё равно будет тормозить же. Если ты ставишь в очередь заданий задания вида "достать очередной элемент, если он не меньше десяти то отправить дальше", то вся херня займёт приблизительно в десять тысяч раз больше времени, чем сама проверка.

Это если на простых итераторах делать. Грамотный подход берет данные большими кусками - N элементов отдал одному потоку, N элементов другому, для этого random access и нужен.
Приведи пример

Мне сейчас спросонья не очень понятна разница между итератором и итераблем в контексте обсуждаемого. Вот реальный пример на игрушечной задачке с одного конкурса:
http://gist.github.com/2922431
foreach(i, cell; taskPool.parallel(cells.keys, 10000 
mds[i] = localmin(cell);

Внутри localmin другие циклы, но распараллеливаю по внешнему, нет смысла делать иначе. Тут cells.keys - массив (ключей хэш-таблицы функция taskPool.parallel видит, что параметр умеет random access и знает длину, она делит данные на куски по 10000 элементов (второй параметр обработка которых становится заданиями для пула рабочих потоков. В качестве основного параметра вместо обычного массива мог быть произвольный random access range, работало бы так же. А вот если бы это был просто input iterator, она работала бы иначе - шла бы по нему последовательно, копируя в буфера по N элементов, и только потом бы раздавала их потокам для обработки.

bleyman

Нет, я имел в виду внешний. Иначе будет очень fine-grain, о чем говорил господин ПЖ.
Ммм, в твоём примере внутренний...
Ты бьёшь на куски _исходные_ данные, затем пушишь каждый кусок в конвеер работающий на итераторах, затем как-то объединяешь результаты.
As opposed to запустить конвеер на исходных данных, получить итератор выдающий конечные данные, затем пытаться распараллелить уже его как-то.
Разница между iterable и input iterator при этом оказывается важна. На вход части твоего алгоритма который отвечает за распараллеливание подаётся iterable, причём ты более или менее знаешь её конкретный тип, то, что она например Collection, то есть имеет длину и позволяет делать random access и слайсы (как врапперы, естественно). Умение распознавать эти свойства, а так же небольшая иерархия интерфейсов, более чем уместны для итераблей. Это а) нужно, б) не мешает так как используется в одном месте — в параллелящем алгоритме.
С другой стороны, the meat of your actual algorithm, этот самый конвеер, работает с единственным типом итераторов — input iterator. Это никак не влияет на распараллеливание, потому что ты это _уже_ сделал, так что каждый инстанс конвеера может дико эффективно работать sequentially. И это дичайше облегчает жизнь программисту, а так же делает эффективность предсказуемой (и эффективной, никакой синхронизации не нужно внутри конвеера!).
А вот если бы это был просто input iterator, она работала бы иначе - шла бы по нему последовательно, копируя в буфера по N элементов, и только потом бы раздавала их потокам для обработки.
Во-первых, как я уже сказал, если у тебя распараллеливание происходит на входных данных, там ты можешь работать с более детальным типом коллекции, na zdorovje, только потом ты получаешь input iterator для каждого слайса и отдаёшь его в последовательную часть алгоритма (инстансы которой работают параллельно).
Во-вторых, даже если бы у тебя и на входе был инпут итератор, распиливание его на чанки последовательно _дешёвое_. Ты не пытаешься пилить выходной итератор, ты пилишь входной, там сырые данные и никаких вычислений, тяжёлые вычисления происходят потом, параллельно.

Dasar

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

bleyman

при этом ты неявно подразумеваешь, что это (эффективная работа с последовательным итератором) возможно для большинства часто встречающихся операций, но это же не так.
Например?
Если ты думаешь про всякие group by, то нифига, мы уже заранее решили что мы ничего не пытаемся параллелить кроме как в цикле по входным данным, а для последовательных операций, ну, чтение из итератора в dictionary не отличается от чтения из массива, зачем тебе может понадобиться читать массив не последовательно? Если ты свой input iterator на каком-то этапе зафорсил и отсортировал, ок, передавай сортированный массив дальше, я не против.

Dasar

Например?
банальный paging
Если ты думаешь про всякие group by, то нифига, мы уже заранее решили что мы ничего не пытаемся параллелить кроме как в цикле по входным данным
не вижу, как из утверждения, что параллелизация делается на верхнем уровне следует, что при это в конвеере делается только одна параллелизация.
в следующем коде будет три раза делаться параллелизация (одна в select-е, одна при сортировке и одна при попарной обработке)

var result =
items
.Select(item => HeavyOperation(item
.OrderBy(element => element.Date)
.ПоПарнаяОбработкаe1, e2) => e2.Date - e1.Date)
.ToArray;

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

karkar

Ммм, в твоём примере внутренний...
В моем примере это самый внешний, внешнее некуда. Синтаксически.
 
Ты бьёшь на куски _исходные_ данные

Да, причем это делается без копирования. Я могу их обработать in-place. Ничего никуда не пушится и не объединяется в подобной ситуации.
 
На вход части твоего алгоритма который отвечает за распараллеливание подаётся iterable, причём ты более или менее знаешь её конкретный тип, то, что она например Collection, то есть имеет длину и позволяет делать random access и слайсы (как врапперы, естественно).

За распараллеливание тут отвечает функция из стандартной библиотеки. В данном случае и языке итерабли особо не нужны - вся необходимая информация о capabilities рэнджа есть в его типе, поэтому та параллелящая функция отлично умеет принимать разные рэнджи. То, что в данном примере она приняла массив, который можно назвать итераблем, это непринципиальная случайность - авторы научили ее принимать массив, так при передаче писать на две буквы меньше.
 
С другой стороны, the meat of your actual algorithm, этот самый конвеер, работает с единственным типом итераторов — input iterator.

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

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

bleyman

в следующем коде будет три раза делаться параллелизация (одна в select-е, одна при сортировке и одна при попарной обработке)
Да. Потому что ты два раза форсишь данные в массив. Каждый раз после этого у тебя получается опять коллекция, а не итератор.
Где именно ты предполагаешь увидеть различие в производительности (между чем и чем, конкретно?) я не очень понимаю, поясни.
@deemon:
> Да, причем это делается без копирования. Я могу их обработать in-place. Ничего никуда не пушится и не объединяется в подобной ситуации.
Ну и в питоне ничего никуда не копируется когда ты бьёшь numpy array на слайсы, например.
Смотри, короче, ты говоришь, вот, это довольно прикольно когда у нас вместо простого input iterator всюду передаются разнообразные рейнджи с разными статически-определимыми интересными способностями. Ну да, довольно прикольно, но это ведь не бесплатно.
Мне периодически нужно писать свои итераторы. Не часто, но всё же. Типа,

def pairwise(iterable):
prev = next(iterable)
for it in iterable:
yield (prev, it)
prev = it

Или более специфические штуки.
Питон говорит мне: вот, чувак, если ты хочешь имплементировать свой итератор — пиши его как input и не парься ни о чём, не пытайся сделать его клонируемым, не пытайся сделать его reversible, не пытайся сделать его random access, у нас принято что если кому-то нужна такая функциональность зачем-то, они совершают свои измывательства над коллекцией с которой они работают, не с полученными из неё итераторами. Так что я беру и пишу, и всё работает, и все счастливы.
С другой стороны, как я понимаю, D как бы говорит мне, э, бро, давай-ка ты для начала сделаешь версию которая даёт random access, а потом если тебе передали не random access range, то сделаешь три или пять версий которые работают с менее понтовыми итераторами. Ну, типа, если ты хороший парень и заботишься о своих юзерах. А если ты ленивая свинья и сделаешь только input iterator, то использование его в любом конвеере имеет хорошие шансы убить всю параллельность, без каких-либо варнингов причём. Мне такие расклады совершенно не по нраву.

karkar

С другой стороны, как я понимаю, D как бы говорит мне, э, бро, давай-ка ты для начала сделаешь версию которая даёт random access, а потом если тебе передали не random access range, то сделаешь три или пять версий которые работают с менее понтовыми итераторами. Ну, типа, если ты хороший парень и заботишься о своих юзерах. А если ты ленивая свинья и сделаешь только input iterator, то использование его в любом конвеере имеет хорошие шансы убить всю параллельность, без каких-либо варнингов причём.
Ну да, разница лишь в том, что питон по умолчанию считает тебя ленивой свиньей и не предлагает делать хорошо, предлагает сразу делать плохо и убивать параллельность. :)
В D основную работу берут на себя комбинаторы из стандартной библиотеки, и если нужная тебе последовательность выражается через них, то нужные свойства она получает автомагически.

bleyman

Ну да, разница лишь в том, что питон по умолчанию считает тебя ленивой свиньей и не предлагает делать хорошо, предлагает сразу делать плохо и убивать параллельность.
Во-первых, параллельность не убивается ни разу, в чём вся и фишка. Есть как бы договорённость, что параллельность может делаться только в те моменты когда у тебя есть зафорсенная коллекция. Этого достаточно практически всегда, ну, потому что когда иначе тебе этого может захотеться, в самом деле? В рамках этой договорённости делать только input итераторы — хорошо.
Во-вторых, программисты всегда ленивые свиньи, это факт. Лень — двигатель прогресса, лень — самое главное положительное качество программиста, которое заставляет его создавать полезные абстракции вместо того, чтобы в поте лица копипейстить код.
Правильный язык должен embrace (идите нафиг в свою Чехию сами, я не собираюсь напрягаться и переводить это слово с сохранением всех нужных смыслов) этот факт и позволять программистам быть ленивыми свиньями как можно более везде. Это правильно и принципиально (быть ленивой свиньёй == быть хорошим программистом и на практике (тебе же придётся работать с чужим кодом, написанным ленивыми свиньями которые просто ленивы, без метафизических обоснований, и ты будешь плакать кровавыми слезами).
Например, в языке С довольно тяжело использовать динамически растущие массивы, включая строки. В результате мы имеем функции с десятками "char some_string[4098];" и блядские односвязные списки повсюду, которые глючат, тормозят, и недоступны для понимания, как я их ненавижу! Переписывание кода на плюсы делает его быстрее в разы и понимаемее в десятки раз.
В D основную работу берут на себя комбинаторы из стандартной библиотеки, и если нужная тебе последовательность выражается через них, то нужные свойства она получает автомагически.

А если не выражается? Почему я вообще должен придумывать как её выразить даже если можно, чтобы потратить кучу времени придумывая и написав хуйпроссышный one-liner в результате, вместо прямого и недвусмысленного input iterator? Давай, вырази мне pairwise из моего примера.
Ох, и ещё, раз мы говорим про производительность, мне это всё немножко напоминает http://yosefk.com/c++fqa/ctors.html#fqa-10.9. If you care about performance, like, really care about having a performant program instead of having a larger supply of arguments for arguing on the internet, the thing you must care first and foremost is _predictability_. You want to have predictable performance, you want to be able to say: well, this pipeline executes on 16 threads in parallel _always_. When you can't say that because it is entirely possible that some lazy pig implemented one of the combinators you use as an input iterator only, and you're depending on the compiler magic to give you performance, you can't honestly claim that you care about performance.
On the other hand, a language/programming culture that allows you to know that so and so shit are lazy input iterators, but splitting the collection where it matters is guaranteed to have them all lazy pipelines running in parallel does allow you to optimize for performance directly.

Dasar

Да. Потому что ты два раза форсишь данные в массив. Каждый раз после этого у тебя получается опять коллекция, а не итератор.
Где два раза?

bleyman

var result = 
  items
  .Select(item => HeavyOperation(item
  .OrderBy(element => element.Date)
  .ПоПарнаяОбработкаe1, e2) => e2.Date - e1.Date)
  .ToArray;

ОК, один раз, в ".OrderBy(element => element.Date)" и там параллелизация внутренняя, то есть не влияет на результат.
У тебя может быть параллельный Select, результат форсится в массив, дальше ты его сортируешь (возможно параллельно внутри дальше ты опять получаешь массив (причём по идее он может иметь особый тип SortedArray, хотя в твоём случае это неважно дальше ты можешь его опять параллельно обработать и зафорсить.
К чему это всё?

Dasar

К чему это всё?
какой тип на входе и выходе каждой функции в таком конвейере?

bleyman

IOrderedEnumerable<TSource> for OrderBy, for example. IEnumerable, not IEnumerator; collection, not iterator. Also, IOrderedEnumerable, like, by the way, dude!

Dasar

[..]Enumerable [..] Enumerable[..]
100% оверхед на выделении памяти (либо доп. проход на подсчет кол-ва элементов)

bleyman

Выделение памяти дико дешёвое, копирование данных дешёвое, да, там 2х константа, но всё равно О(1).
Это. Постой-ка. Как ты собираешься имплементить OrderBy без дополнительной памяти? Перехуячить оригинальный массив никому ничего не сказав? Не, я бы тоже был бы рад языку который can into escape analysis и uniqueness typing, но надо же всё-таки хоть как-то оставаться в здравом рассудке...

Dasar

копирование данных дешёвое
а что тогда долгое? :confused:
ps
Миллион раз сложить много раз быстрее, чем миллион скопировать
pps
> Как ты собираешься имплементить OrderBy без дополнительной памяти?
У OrderBy, если длина известна, то сложность O(n*ln n если длина не известна, то O(n*(1+ln n
У конструкции "ПоПарнаяОбработкаe1, e2) => e2.Date - e1.Date).ToArray" если длина известна - сложность O(n если длина неизвестна, то O(2n)

Dasar

если длина неизвестна, то O(2n)
При параллелизации, оверхед будет еще больше

bleyman

> а что тогда долгое?
Обработка данных. Сама сортировка. Вот например в жаве все сортировки сейчас используют TimSort (by Tim Peters, originally implemented in CPython который по сути своей является хитроумным merge sort и таки требует O(n) дополнительной памяти. Но дичайше быстрый зато.
У конструкции "ПоПарнаяОбработкаe1, e2) => e2.Date - e1.Date).ToArray" если длина известна - сложность O(n если длина неизвестна, то O(2n)
Ну я же комбинатор для неё написал на петоне в этом же треде же. O(2n) is not a thing, O(2n) == O(n и константа 2 в сто раз меньше в этом контексте чем константа в O(n log n) в самой сортировке.
EDIT: > При параллелизации, оверхед будет еще больше
С чего бы вдруг? Исчо раз, мы всегда параллелизируем по исходным данным, не по конечному итератору. Поэтому ты разбиваешь исходные данные на 16 блоков, запускаешь 16 пайплайнов их обрабатывающих, когда все закончили, объединяешь результаты в одном треде (потому что нафиг вообще пытаться оптимизировать копирование raw data в один массив, это быстро и так, ты в этом ограничен memory bandwidth (16Gb/s or something like that.

bleyman

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

Dasar

константа 2 в сто раз меньше в этом контексте чем константа в O(n log n) в самой сортировке.
С таким подходом быстрое и энергоемкое ПО не напишешь.
На данном примере потери составят 10% на миллионе записей (2 лишних прохода vs log2 1e6 == 24)

bleyman

> 2 лишних прохода vs log2 1e6 == 24
WHAT. THE. FUCK. ARE. YOU. TALKING. ABOUT.
Ты код квиксорта читал хоть раз? Нет, не читал, вот иди и прочитай, прежде чем нести херню.

Dasar

ты в этом ограничен memory bandwidth (16Gb/s or something like that.
так процессор молотит вычисления ровно с такой же скоростью. 4GHz по 1такту на 4байтное число.

Dasar

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

bleyman

О да, чувак. И если ты внезапно пойдёшь и прочитаешь код квиксорта в твоей любимой libc++, ты обнаружишь, что этот код делает множество стрёмных вещей.
Так что твои рассуждения про энергоёмкость идут мимо. И особенно сравнения энергоёмкости тупого копирования данных с энергоёмкостью квиксорта, у которого, на секундочку, случается сравнение и conditional jump (а то и два) на каждом уровне, что означает pipeline eviction for one of the branches.
Я как-то потерял нить, о чём мы вообще спорим сейчас? Что квиксорт тормозит в десятки если не сотни раз (вдобавок к log n) по сравнению с тупым копированием данных в памяти, и жрёт сопоставимо больше электричества?

Dasar

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

bleyman

В первом вариант оптимизация исходит из того, что у последовательности известна длина,
второй вариант оптимизации исходит из того, что передается лишь enumerable

What. Я сказал что я не против небольшой иерархии классов для enumberable, и что вот конкретно распараллеливающий комбинатор может быть написан несколько раз, для каждого типа IEnumerable. Like, IOrderedEnumerable, что-то ещё для random access, ICollection, I guess.
Мой поинт состоит в том, что дальше, после того как ты распараллелил исходные данные и скормил их слайсообразно шестнадцати конвеерам, тебе не нужно думать ничего про имплементацию конвееров. Ты знаешь что они все sequential, и всё такое. Когда ты — программист, ты имплементируешь свой итератор дико тупо как input iterator, не заморачиваясь ни о чём, у тебя Договорённость — заморачиваются те, у кого есть forced collection.
EDIT: Also, я как бы продвигаю C# модель и говорю только о ней. Меня радуют C# custom iterators. Что именно ты хочешь увидеть от меня, типа, в этом аспекте?

karkar

А если не выражается? Почему я вообще должен придумывать как её выразить даже если можно, чтобы потратить кучу времени придумывая и написав хуйпроссышный one-liner в результате, вместо прямого и недвусмысленного input iterator? Давай, вырази мне pairwise из моего примера.
auto pairwise(RR src) if (isInputRange!R)
{
static if (isForwardRange!R)
return zip(src, src.save.drop(1;
else {
auto prev = src.front;
src.popFront;
return src.map!x) { auto res = tuple(prev, x); prev = x; return res; });
}
}

Не особо сложнее змеиного оригинала. Если нужный рэндж просто не выражается или лень думать - делай простой итератор, просто он будет иметь чуть меньше полезных свойств и витаминов.
В моем случае если передать в pairwise простой input iterator, получишь такой же. А если forward или random access - их свойства сохраняются.

bleyman

Ну то есть ты заимплементил в результате два разных алгоритма.
Если тебе ещё захочется поддерживать bidirectional iterators и forward iterators надобно будет написать ещё два вдобавок, наверное.
При том, что это всё не нужно, как мне кажется. Вполне отлично можно жить если эта вся функциональность есть только у итераблей (которым в результате достаточно заимплементить random access и слайсинг а все итераторы — только инпут, только хардкор.

karkar

Форвард и bidirectional тут автоматом уже получились.
Вполне отлично можно жить если эта вся функциональность есть только у итераблей (которым в результате достаточно заимплементить random access и слайсинг)

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

bleyman

> Форвард и bidirectional тут автоматом уже получились.
Не-не-не, совсем не получились же, ты тут варантность попутал, братишка. Если тебе дают RAIterator, то конечно он автоматически и Forward (aka cloneable, хотя хз кстати, из твоего кода не следует очевидным образом и bidirectional. А вот если тебе дают только bidirectional iterator, твой код выдаёт наружу input iterator, некошерненько, э?
Поэтому если ты неленивый программист, тебе нужно заимплементить вдобавок методы bidirectional iterator если переданный тебе итератор is bidirectional.
Ещё я бы развлёкся наблюдением тебя пытающегося так же поддержать варианты типа bidirectional but not forward.
Ты же неленивый программист? Вот, давай, пиши!
> Ну вот pairwise в них не окажется, и все, плакать, колоться, жрать последовательный проход?
Тебе не нужен pairwise на уровне итераблей. This is known.

karkar

Не, я не настолько неленивый. :) Сперва обоснуй, что bidirectional но не random access рэндж реально может встретиться и понадобиться. Три основных и часто встречающихся - чисто input, forward и random access - были учтены без особых сложностей.
Тебе не нужен pairwise на уровне итераблей. This is known.

А на каком уровне он нужен?

bleyman

Сперва обоснуй, что bidirectional но не random access рэндж реально может встретиться и понадобиться.
Двусвязные списки, обход дерева какого-нибудь...
Не знаю, как-то странно вдруг делить типы итераторов на часто встречающиеся и редко встречающиеся. Если ты поддерживаешь какой-то тип, то ты и все остальные программисты на этом языке его поддерживаете во всех своих комбинаторах, кровь из носу. Если не поддерживаете — то не поддерживаете нигде потому что незачем, всё равно ничто другое включая стандартную библиотеку с ним полноценно работать не сможет.
Тебе не нужен pairwise на уровне итераблей. This is known.
А на каком уровне он нужен?
На уровне инпут итераторов. Тебе почему инпут итераторов не всегда достаточно? Потому что ты хочешь параллелить вещи в основном. Уметь параллелить вещи достаточно и удобно на уровне коллекций (то есть итераблей). Уже зафорсенных конечно, тебе не хочется поломать всю свою параллельность случайно зацепив какие-то сложные вычисления с синхронизацией. Ну и зачем тебе тогда pairwise которая вместо того, чтобы вернуть коллекцию, возвращает типа враппер над ней?

karkar

Хе, сейчас проверил. Моя реализация уже сохраняет свойство bidirectional, ничего дописывать вообще не нужно. Потому что в текущей иерархии рэнджей всякий bidirectional является forward'ом, для них у меня применяется zip, который делает все необходимое.
Код проверки, так чисто полюбоваться:

struct ZZ // bidirectional but not random access
{
int a,b;
int front { return a; } // input
bool empty { return a > b; }
void popFront { a++; }

ZZ save { return ZZ(a,b); } // forward

int back { return b; } // bidirectional
void popBack { b--; }
}

void show(RR r)
{
static if (isInputRange!R) writeln("input");
static if (isForwardRange!R) writeln("fwd");
static if (isBidirectionalRange!R) writeln("bi");
static if (isRandomAccessRange!R) writeln("random");
writeln(r);
}

void main(string[] argv)
{
auto z = ZZ(1,5);
show(z);
show(z.pairwise);
[1,2,3][].pairwise.show; // test for a random access range
}

выводит
 
input
fwd
bi
[1, 2, 3, 4, 5]
input
fwd
bi
[Tuple!(int,int1, 2 Tuple!(int,int2, 3 Tuple!(int,int3, 4 Tuple!(int
,int4, 5)]
input
fwd
bi
random
[Tuple!(int,int1, 2 Tuple!(int,int2, 3)]
Оставить комментарий
Имя или ник:
Комментарий: