пятница, 9 сентября 2016 г.

[prog.memories] Путь к ООП как осознанная необходимость...

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

На первом курсе универа нам преподавали программирование в рамках всего одного предмета, емнип, "Методы и средства конструирования программ". На Pascal-е. Сначала на Robotron 1715 без жестких дисков и 64K памяти, где мы использовали Turbo Pascal 3.0. Затем на IBM XT (и из аналогах -- EC1840/1841), уже с винчестерами, 640K RAM и Turbo Pascal 5.0-5.5. В качестве лабораторных очень простые вещи -- методы решения уровнений, сложение/перемножение/обращение матриц, методы сортировки и тому подобное.

И вот к концу первого курса я решил повыпендриваться и в качестве лабораторки по теме "Работа с текстовыми файлами на языке Pascal" вызвался сделать не больше, не меньше, но нормальный экранный текстовый редактор. Под "экранным" понимался обычный многострочный текстовый редактор, в котором передвигаешься по тексту стрелками, вводишь и удаляешь текст в любом месте. В общем то, что было в том же самом Turbo Pascal. Или в редакторе Norton Commander-а.

Сейчас, наверное, размах моей наглости и самомнения оценить сложно. Ведь многострочный TextEdit в виде готового UI-контрола сейчас доступен везде. А тогда даже сделать простое перемещение курсора по всему полю экрана и отображение нужного тебе символа в этом знакоместе было нетривиальной задачей, для решения которой в стандартной библиотеке Turbo Pascal-я не было штатных инструментов. Плюс никто нас не учил тому, как правильно писать большие программы, а объем самых больших программулек, написанных мной к тому времени, вряд ли превышал пару тысяч строк кода.

В общем, для реализации этого редактора очень многое пришлось делать с нуля самостоятельно. И работу с клавиатурой (через BIOS), и работу с экраном (через чтение/запись видеобуфера), и менюшки, и диалоги, и, естественно, сам редактор. Полная перекомпиляция проекта занимала около 45 секунд. При том, что Turbo Pascal компилировал код с очень приличной скоростью (что-то порядка 500 строк в секунду), это означало, что я наколбасил приблизительно 20K строк кода. Для тогдашнего Pascal это было не сложно, язык ну очень уж многословный.

Что мне довольно быстро стало мешать, так это наличие функций с большим количеством аргументов. А т.к. нам уже объяснили в чем достоинства структрного программирования без goto и глобальных переменных, то протягивания состояния выполнялось через аргументы функций. По мере разрастания кода некоторые функции, внутри которых выполнялись какие-то сложные действия, стали получать под 10 аргументов и даже больше. Со временем это стало проблемой. Хорошо еще, что в Pascal-е была статическая типизация и очень шустрая компиляция. Поэтому, если в какую-то функцию требовалось добавить еще один параметр, то довольно быстро обнаруживались все куски кода, которые подлежали правкам.

Т.к. писать большие программы никто не учил, пришлось на ходу переизобретать то, что уже было известно. Связанные по смыслу аргументы объединялись в структуры (например, координаты и размеры окна, которые раньше задавались четырьмя параметрами -- x, y, cx, cy, стали упрятываться в одну структуру Rectangle). Что позволило существенно уменьшить количество аргументов в функциях. А это очень упростило работу с кодом.

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

Текстовый редактор я, в итоге, сделал. Лабу сдал. И переключился с Pascal-я на C. Довольно быстро выяснилось, что C -- это совсем другой мир. Не такой теплый и уютный, как Pascal. Понятие "отстрел ноги" стало ощущаться практически на физическом уровне :)

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

Так я столкнулся с проблемой сохранения инвариантов. Хотя, понятное дело, тогда я даже понятия не имел о том, что такое инварианты объекта. Но незнание терминов же не избавляет от самой проблемы :) А вот хорошее решение для нее обнаружилось только тогда, когда мне довелось познакомиться с ООП и одним из краеугольных столбов ООП -- инкапсуляцией.

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

В общем, в чем суть сей байки. В начале 90-х мне повезло потрахаться с чистым процедурным программированием без привычных многим современным разработчикам средств разработки и библиотек на все случаи жизни. Когда базовые библиотеки пишешь сам и ограничен только структурным и модульным программированием, трудоемкость и неблагодарность этого занятия становится очевидна очень быстро. А потом в твои руки попадает ОО-язык и ты сразу же понимаешь, насколько все то же самое делается проще и удобнее. Причем понимаешь это не с чужих слов, а на основании собственного, пусть и небольшого опыта. И то, что даже небольшой собственный опыт позволяет тебе оценить, насколько же ООП упрощает жизнь, убеждает в достоинствах ООП еще сильнее.

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

четверг, 8 сентября 2016 г.

[prog.flame] Давно что-то не писал про ООП vs ФП, надо исправиться... ;)

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

Понятно, что ООП -- это никакая не серебрянная пуля. И даже на заре проникновения ООП в мейнстрим (по крайней мере в наших Палестинах) было очевидно, что на ряде задач выигрыш от ООП может быть только если задача объемная и предполагает развитие в течении длительного времени. Помнится, некоторые аппологеты, вроде Гради Буча, даже предупреждали, что с ООП придется потратить гораздо больше сил и времени по сравнению с процедурным подходом на начальном этапе разработки. Т.е. на C или на Pascal-е вы бы уже могли написать худо-бедно работающий прототип, а на C++, Eiffel или SmallTalk еще бы только-только завершали фазу ОО-анализа и, возможно, фазу ОО-проектирования. И, скорее всего, не написали бы еще никакого кода.

Тем не менее, при всех своих проблемах ООП всегда (по крайней мере так было в начале 90-х) крутился вокруг практических вещей. Вот здесь у нас будет класс Строка. Ok, понятно что это и зачем. Вот здесь -- класс Файл. Снова Ok, снова понятно что и зачем. Вот здесь -- класс COM-порт. И снова Ok, снова все понятно. Аналогично с классами Меню, Диалог, Конфиг, Таймер, Нить и т.д. И вот уже из понятных частей собирается GUI-приложение для работы с неким внешним устройством через COM-порт.

Поэтому ООП было более-менее просто объяснять людям. Мол, смотри, у тебя будет COM-порт. Мы сейчас точно не знаем, что именно будет у него внутри, но более-менее понимаем, как с ним работать. Поэтому давай сделаем набросок класса COM-порт с несколькими очевидными публичными методами и пойдем дальше... На какой-то N-ой итерации мы возвращаемся к нашему классу и уточняем еще чуть больше. А потом еще и еще раз.

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

Тогда как с ФП лично мне таких объяснений, которые бы отталкивались от практики, особо не попадается. Если говорить совсем грубо, то получается что-то вроде:

-- Вот смотри, у нас есть функция f a b -> c...
-- А что это за функция? Для чего она?
-- Это не важно. У нас просто есть функция f a b -> c. У нее два аргумента. Но, на самом деле, ее можно представить как последовательность функций с одним аргументом f a -> b -> c...
-- И что?
-- И мы может делать каррирование, когда мы из f a b -> c, получаем g b -> c с зафиксированным значением аргумента a. А все месте нам это дает возможность композиции функций!
-- Композиции функций для чего?
-- Для чего угодно. Вот смотри, допустим, нам нужно применить к данным функции x, y и z...
-- К каким именно данным и что именно делают функции x, y и z?
-- Это не важно, важно то, что через композицию мы можем сделать так, что разультат x идет в y, а разультат y идет в z. Мы просто описываем композицию функций декларативно в коде и все.
-- Что все? Какие у нас данные? Как именно мы их обрабатываем? Зачем мы это делаем?
-- Это не важно, я тебе принцип объясняю.

Вот, скажем, как вам такая статья на Хабре: Монады и do-нотация в C++? Но не просто как статья, как демонстрация принципа, на котором можно связать в цепочку последовательность операций. Например, B*x + C, где B и C -- это матрицы, а операции умножения и сложения могут завершаться неудачно.

Собственно, сей пост не является попыткой бросить камень в само ФП. Как по мне, так изрядная часть фишек ФП оказалась в императивном мире давным давно (например, если посмотреть на языки SmallTalk и Ruby, в которых блоки кода есть ни что иное, как лямбды, а изрядная часть методов в стандартной библиотеке базируется на использовании функций высших порядков). И использовалась программистами задолго до возникновения хайпа вокруг ФП, причем многие до сих пор не подозревают об этом, дергая Enumerable#map. Ну и вообще, очень многие вещи из ФП вполне себе просты и понятны, если их объяснять без чрезмерного использования математических формул :)

Речь о том, что рекламе ФП и раньше недоставало приземленности. И сейчас ничего особо не изменилось, как по мне. Поэтому если человек не фанат изучения новых парадигм и языков, и его не прет от реализации моноидов на шаблонах C++, он не тащится от того, насколько генерики+трейты в Rust-е удобнее генериков в Java, а ему тупо нужно взять набор байтиков отсюда и переложить их вот туда, то осознать полезность фишек ФП ему сейчас ничуть не проще, чем 10 лет назад. Имхо, конечно.

PS. Кстати говоря, ничуть не удивлюсь, если для современной молодежи, которая начала учиться программировать сейчас или даже лет пять назад, ситуация видится прямо противоположной. Вероятно, для нонишних новичков в программировании базисом, с которого они стартуют, являются такие вещи, как функции высших порядков, алгоритмы вроде foldr/foldl/zip, алгебраические типы данных и т.д. А вовсе не правила структурного и модульного программирования, с осознания важности которых довелось начинать моему поколению.

среда, 7 сентября 2016 г.

[prog] Mxx_ru-1.6.13 с поддержкой clang под Windows

Сделал очередной релиз своей кроссплатформенной системы сборки C/C++ проектов: Mxx_ru версии 1.6.13.

Установить Mxx_ru можно командой gem install Mxx_ru

Обновить Mxx_ru можно командой gem update Mxx_ru

Так же Mxx_ru можно загрузить с SourceForge (gem-файл).

В этой версии добавлена поддержка тулсета clang_msvc. Недавно вышедший llvm-3.9.0 у меня впервые нормально заработал под Windows в связке с Visual C++ 14.0 (update3). Т.е. транслятор от llvm, а все заголовочные файлы, библиотеки и линкер -- от VC++.


Получается, что clang сейчас поддерживается в нескольких видах: под Unix-ами и под Windows поверх VC++. Видимо, со временем надо будет в Mxx_ru добавить для clang-а что-то вроде gcc_port. В версии 1.6.13 ничего подобного пока нет. Сначала поднакопим опыт, а дальше будет видно.

понедельник, 5 сентября 2016 г.

[prog.flame] Яркий пример того, почему инструменты вроде SObjectizer-а нужны, а на самом деле нет

Примером служит вот эта тема на LOR-е. Дабы не нужно было ходить по ссылке (хотя сходить, почитать комментарии, можно) скопирую стартовое сообщение:

Есть общий вопрос по очередям, по причине изобретения своего лисапеда. Как в нодах с воркерами регулируется нагрузка процессора? Хочется «чтобы было хорошо», но без крайностей вроде полного дублирования шедьюлера операционки.

Самое простое, это сделать воркеры однотредовыми и выгребать за раз по 1 задаче. Но даже если задача «тяжелая», у нее могут быть операции I/O, где процессор простаивает. Первое что приходит в голову - сделать воркеров в пару раз больше чем ядер, это вроде решит проблему с размазыванием нагрузки на проц.

А как быть если задача «долгая», но не сильно грузит CPU? Например, сканирование URL. Там время уходит на скачивание файла, а проц стоит. И что не очень приятно, если прилетит пачка таких задач (больше чем воркеров), они могут например тормознуть отправку писем (более приоритетные задачи).

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

UPD. Воркер - процесс node.js, то есть кооперативность на уровне IO там есть.

Предлагаю абстрагироваться от того, что автору LOR-овского топика нужно решение именно под node.js. Сама по себе проблема раскидывания разнородных тасков по рабочим нитям в тех или иных вариациях встречается весьма часто. Даже псевдозадачка из статей о SObjectizer-е для Хабра (которая про проверку email-ов) -- она ведь из той же оперы по сути.

И SObjectizer мы создавали для того, чтобы подобные задачи решались проще и быстрее. Отсюда и такая штука, как диспетчеры, которые позволяют "по щелчку" привязывать агентов-воркеров к наиболее подходящему рабочему контексту. Нужна рабочая нить, на которой мелкие агенты будут очень быстро инициировать асинхронные IO-операции -- пожалуйста. Нужен пул рабочих нитей, на которых тяжеловесные агенты будут выполнять нагружающие CPU операции -- пожалуйста. Нужны отдельные нити для работы со сторонним синхронным API -- да не вопрос. Ну и доступные из коробки очереди сообщений для общения агентов друг с другом. А так же всякие ручки для настройки и контроля всего этого.

Так что задачек, в которых инструмент вроде SObjectizer-а облегчил бы жизнь разработчикам, не так уж и мало. Собственно, массовые случаи успешного применения Erlang-а и Akka это подтверждают.

Проблема, однако, в том, что когда разработчик сталкивается с подобной задачкой, он начинает на нее смотреть через призму своих собственных стереотипов, фобий и возможностей. Застрянет у такого разработчика в башке какая-нибудь идея фикс и все. Ничего больше не воспринимается. Зачем брать что-то чужое готовое, если можно слабать на коленке свой лисапед, у которого не будет фатального недостатка? ;)

воскресенье, 4 сентября 2016 г.

[prog.c++] Нововведения в SObjectizer-5.5.18: расширение статистики для диспетчеров

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

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