Езда на медведях как метод объектно-ориентированного программирования

Mar 21, 2011 15:19

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

После наболевшего от прожужжанных «паттернами» ушей ехидного предыдущего поста, последовавшего эпического срача в твиттере, поста zahardzhan и приезда сильно увлеченного объектно-ориентированными свистелками иностранного профессора, я даже на несколько дней засомневался в том, что адекватно воспринимаю происходящее, поскольку мои ответы на вопросы «что не так с X?» собеседников не устраивали. Однако, на прошедшей неделе я случайно для себя открыл замечательный метод рассуждений, позволяющий делать забавные умозаключения о современном state of art, и сейчас я немного их сюда повыписываю, стараясь аргументированно указать видимые мною недостатки.

Я бы рад снова начать с «паттернов», но ведь скажут, что я помешанный. Потому начну с (не) биологии. Итак.

С рождения человечества и до девятнадцатого века люди в качестве тяги использовали исключительно лошадей (и собак, а особенно увлечённые ещё и медведей). Однако, с появлением паровозов, а потом и автомобилей, желающих кататься на почтовых становилось всё меньше. Почему? Я думаю, на то три основные причины. Во-первых, даже самые ранние автомобили были сравнимы выходной мощностью с лошадьми (иначе откуда взялись «лошадиные силы»). Во-вторых, вырастить вдвое более быструю кобылу, жрущую втрое меньше сена и не требующую конюха - шансов мало, в то время как прогресс автомобилестроения обещал сделать достижимыми очень высокие планки производительности (а потом и экономичности). И, наконец, автомобиль - куда более предсказуемый в управлении и обслуживании объект, нежели лошадь.

Перестали ли люди ездить на лошадях (собаках, медведях)? Нет (нет, вроде да). Почему? В экстремальных северных условиях собаки ведут себя предсказуемее техники, и по степи, где нет бензоколонок, кататься на лошади приятнее, например. Это объективно. Но ещё бывают всякие адепты натурализма, утверждающие, что удобрять асфальт лошадиными навозом очень полезно для окружающей среды. Я уверен, что и во время переходного периода между лошадьми и автомобилями существовали клубы приверженцев реакционных мнений о том, что автомобили не нужны, а лошадей хватит для всего. Только штука в том, что если один и тот же результат можно получать биологически и автоматически, то второй вариант производственного процесса рано или поздно выигрывает. Что, однако, не мешает существовать кругу добровольных ценителей и кругу вынужденных пользователей «натурального».

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

Языки, построенные на стековых виртуальных машинах, в этом плане очень показательны. С точки зрения разработчиков на них, авторы языков вроде Haskell и Agda сделали «ужасную» вещь - они отобрали у программиста все его привычные инструменты.

А после стали понемногу добавлять всякие «странные вещи», типа ADT, монад, Typeable, type families и индукцию по структуре (вместо неограниченной рекурсии). «Jonny the programmer» недоволен, как писать программы - непонятно, Джонни чертыхнулся и пошёл дальше писать на дотNet. Но те, кто таки поддался соблазну, внезапно обнаружили, что 80% полезных участков кода даже не требуют Тьюринг-полноты, а 19% из оставшихся не требуют отказа от строгости в типах (например, потому что они вдруг становятся рекурсивными). Да и вообще, плюсы вписывания всего, что только можно, в этот новый базис приятны: о программах можно автоматически (ыц!) рассуждать, можно их автоматически (ыц! ыц!) трансформировать (смотри, например, на трансформацию в векторные операции у Data Parallel Haskell), действительно сложные и неразрешимые вещи (IO и рекурсивные типы, например) можно чудесно изолировать и статически гарантировать эту изоляцию (то есть автоматически (ыц! ыц! ыц!) её проверять). Правда остаётся ещё один процент программ, где нужно очень аккуратно работать с ресурсами, или где всё действительно должно быть Dynamic и типы бесполезны, и обозначенные языки туда (пока?) лучше не совать.

Однако, оказывается, что эти «странные вещи» ещё и позволяют очень просто делать то, что раньше считалось сложным. Тут Джонни начинает крутить головой по сторонам, облизываться и требовать, чтобы и в его любимом языке дотNet появился вывод типов, и анонимные функции, и какие-нибудь монады, и ещё ADT, и ещё континюэйшоны, а теперь хорошо бы ещё и тайпклассы, да, да и это тоже было бы не плохо, и вывод типов (ой, он уже есть, но теперь есть тайпклассы и надо его сделать умнее), о, и ограниченные типы тоже хочу, и ещё нормальную редукцию мне, а не только макросы, и от зависимых типов не откажусь... В итоге, в погоне за упрощением методов выражения ограниченного класса типичных идиом, вместо solidного языка получается каша из фич, надёрганных из разных мест (например, делегаты и анонимные функции в C#).

Но Джонни теперь может в три раза короче описывать парсеры, так какие проблемы? Такие, что, если в язык, в котором было нельзя автоматически рассуждать о программах, добавить штуки, о которых можно автоматически рассуждать, то о языке рассуждать всё равно не получится. В общем, досыпали синтаксического сахара, а польза близка к нулю. Что же делать, если Джонни вдруг понял, что иногда уж очень хочется сбросить на компилятор однообразную работу.
- А! А давайте введём специальный тип модулей, в которых будем использовать только те инструменты о которых можно автоматически рассуждать! - в порыве энтузиазма восклицает Джонни.
- Мистер, ваше предложение означает, что эти модули вы будете писать на Haskell.
- Ой.

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

Итак, если не запомните ничего другого.
* Eсли один и тот же результат можно получать биологически и автоматически, то второй вариант производственного процесса рано или поздно выигрывает.
* «Странные вещи» из функциональных языков хороши не тем, что на них коротко выражаются «паттерны», а тем, что о них можно автоматически рассуждать.
* Добавление этих «странных вещей» в существующие языки ничего полезного в этом аспекте не добавляют, поскольку, как правило, в этих языках уже достаточно мусора, о котором не получается автоматически рассуждать.
* Автоматические рассуждения о программах очень помогают писать хорошие программы. Доказывать корректность программ (в разумных пределах) намного проще и полезнее, чем об этом рассказывают.
* Сплошная «биология» - маразм, но выражать страшные вещи, не лезущие в известные разрешимые системы типов, иногда тоже хочется, потому от неё (пока?) никуда не деться.

А вот теперь снова про «паттерны».

Где-то на седьмой странице срача в твиттере я написал что-то интересное про обучение паттернам в ВУЗах, если немного переформулировать написанное там, то получится что-то типа «Учить проектированию по паттернам - достаточно бессмысленное занятие, поскольку невозможно адекватно оценить результаты. Всегда найдётся студент, сделавший задание лучше, чем вы можете себе представить, и без использования ваших любимых паттернов. Оценить абстрактные метрики типа расширяемости и масштабируемости невозможно, поскольку бывают решения заданий, где на каждый аргумент против конкретного проектного предложения всегда найдётся маленькое изменение в коде, его аннулирующее.»
Когда я несколько лет назад начал читать студентам курс программирования для POSIX-совместимых систем, я, например, думал, что знаю все удачные приёмы руления массивами структурок, используемых системным вызовом poll (к тому времени я прочитал уйму исходников, использовавших несколько простых «паттернов» работы с pollом). Однако очень скоро я увидел решение придуманной мною лабораторной с необычно простым, но эффективным аллокатором, заточенным конкретно под выданное задание. Да, при работе с памятью (почти) всегда можно быстро придумать какую-нибудь гадость, делающую любому аллокатору не очень хорошо, или порассуждать, что эта штука плохо обобщается на массивы структур, обладающих такими-то и такими-то свойствами. Но приёмом, который я прочёл в том решении, я сам до сих пор иногда пользуюсь, жонглируя структурками с файловыми дескрипторами.

Мораль? Очень приятно думать, что ты умный, потому что знаешь тонкости между MVC и MVVM, чем отличаются друг от друга поведенческие диаграммы в UML и как диалектика Гегеля связана с Марксом. Это всё очень ценные знания. Не менее ценна уверенность в том, что ты обладаешь общим взглядом на вещи в которых совершенно не разбираешься.

Когда я учился на втором курсе, то, вроде бы, я был единственным из потока, кто дома пользовался svn'ом для локальных нужд. На третьем курсе (ближе к концу) я почти ежедневно пользовался mercurial'ом и git'ом (и иногда клонировал репозитории на других экзотических системах контроля версиями типа darcs или bazaar), в то время как на лекции по методологиям разработки ПО на четвёртом курсе (рассказывали там про всякий ЭксПи, Аджайл и etc) сознались в хотя бы единократной встрече с svn'ом всего трое (включая меня). Примерно на третьем же курсе я решил переехать с qwerty на dvorak (на самом деле, даже ещё будучи на втором, даже пост в жж нашёл), каждый день менял менял окружение рабочего стола (GNOME - Xfce - icewm - openbox - fluxbox - wmii - ratpoison - awesome - xmonad), шеллы (bash - zsh), текстовые редакторы (gedit - nano - vim - emacs, yi), ежедневные языки программирования (C/C++ - Python, Lua, bash/zsh - OCaml - Haskell) игрался с десятками операционных систем от OpenBSD до Plan 9 и от Bluebottle до ЛИСП-машин. В общем, я очень долго гордился тем, что умел пользоваться тем, о чём другие никогда и не слышали.

Сразу после сдачи госэкзамена по философии она чуть было не стала казаться мне чем-то интересным, но, с углублением обратно в теорию типов, это заблуждение как-то быстро улетучилось. Аналогичным образом, моя гордость в отношении владения тем, о чём другие даже и не слышали, исчезла где-то в начале четвёртого курса, когда внезапно обнаружилось, что в моих познаниях в CS много больших белых пятен.
Оно и не мудрено обидеться, когда тебе говорят, что то, что ты считал мега-крутым и бодро и увлечённо узучал несколько месяцев (книжка ведь такая толстая!), внезапно оказывается тривиальным следствием из чего-то ещё.

В общем, если «паттерны» - полезная методология, то «автоматное программирование» - невероятный рокет сайнс. Такие дела.

В промежутке времени между предыдущим постом и этим, я прочёл несколько интересных книжек, в частности, «Мифический человеко-месяц» (второе издание, это важно) и «Joel on Software». При чтении первого произведения в голове появляется замечательная филососфская модель, приводящая к появлению «паттернов», и становится очевидным механизм распространения этой заразы (HR, ищущие в резюме не знания, а ключевые слова, и менеджмент, готовый продать душу дьяволу за любой механизм контроля над трудом программистов, будь это «UML» или всеофисные хороводы вокруг кофеварки по средам). Во второй книге (которая, что забавно, является печатной версией блога автора), совершенно случайно есть замечательная статья о «Методологиях» (кто не прочитал - сам себе злобный буратино), во многом подтверждающая обозначенные выше наблюдения.

Итак, если не запомните ничего другого.
* Приятно считать себя умным, разбираясь в уйме частных случаев.
* «Паттерны» - частные случаи.
* Всегда находятся элегантные решения без «паттернов».

Теперь немного конструктивизма.

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

По порядку.
* Инкапсуляция. Что под этим термином понимают разные люди - им одним ведомо. Метки видимости ли (это не свойство одних только ООП языков), абилити собирать сложные вещи из простых ли (а это свойство программирования вцелом). Не говоря уже о том, что существуют языки, называющиеся объектно-ориентированными, но как-то обходящиеся без обозначенного механизма.
* Полиморфизм. Даже в Википедии написано, что это «Один интерфейс, множество реализаций». Короче, реально хочется не типы под квантор (под какой - это, кстати, ещё вопрос) уметь вносить, чтобы логику высших порядков из интерфейсов строить, а просто разные функции с одинаковыми именами для разных типов аргументов вызывать.
* Наследование. А подумаем для чего его вообще используют.
** Во-первых, чтобы делать интерфейсы. Мы только что поняли, что это можно и без наследования.
** Во-вторых, чтобы эмулировать копроизведение (логическое «или») для нескольких типов.
Действительно, произведение типов делается простым «включением в».

class AandB {
A fst;
B snd;
}

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

class AorB {
// common interface
}

class A : AorB {
// interface impl
}

class B : AorB {
// interface impl
}

** В-третьих, чтобы эмулировать передачу функции в качестве аргументов, в другую функцию. Also known as «мне лень дважды писать этот код в разных классах».

class Foo {
X foo(X x) { return bar(x); }
X bar(X x);
}

class Bar : Foo {
X bar(X x) { return some_x; }
}

** В-четвёрхых, для какой-то нетривиальной комбинации предыдущих трёх.
** Для ещё какой-то чёрной магии.
Проблема с наследованием как раз в последнем пункте. О произведениях и копроизведениях типов легко рассуждать автоматически. О передаче функций в качестве аргументов тоже давно научились. Даже об интерфейсах можно формально судить. А о нетривиальных комбинациях перечисленных, да ещё и с осознанием возможности существования чёрной магии - почти невозможно.

Вообще, я считаю, что понятие редукции ключевое в любом языке. Как только вы собираетесь начать писать оптимизирующий компилятор, так она сразу откуда-то выскакивает, поскольку единственный известный способ оптимизации - выкидывание из результирующей программы как можно большего числа «лишних» операций. Так как обычно при этом хочется сохранить эквивалентность (в каком-то смысле) исходной и соптимизированной программ, то нужны какие-то правила преобразований. Да, явного notion of reduction типа бета-редукции обычно недостаточно, чтобы всем сделать хорошо. Например, понять, что (1 + m + 2) эквивалентно (3 + m), в автоматическом режиме достаточно тяжело, пользуясь только определением операции сложения. Только вот без редукции с простой семантикой в любом случае ничего не получается. Например, хотелось бы научится частично вычислять отнаследованные классы, подставляя виртуальные «функции-аргументы» куда следует. Да что-то сложно, глядя на код, пойди ещё разберись, передача в качестве аргументов там, или ещё что-то. Хотелось бы местами прятать копроизведения в tagged unions, чтобы экономить на виртуальных таблицах, да что-то и тут муть какая-то получается. Хотим частично вычислять выражения внутри функций, да как только встречается вызов метода класса, так тут же всё и обламывается, ибо уж больно сложный динамический диспатч, и пойди разберись какую тут функцию реально вызывают. В общем, ничего кроме тривиальных арифметических операций, да выкидывания недостижимого кода, в компайлтайме-то и не соптимизировать. Пичалька.

Ну ладно, зато может быть существует полезное применение этой чёрной магии в виде наследования, не являющееся одним из обозначенных механизмов? Иначе чего ради мы пожертвовали столькими полезными фичами? Да, пожалуй, может и существует (и в динамических языках типа JavaScript или Python эта чёрная магия даже иногда используется). Но почему-то, вместо того, чтобы показывать эти чёрные трюки, адепты ОО программирования сами начинают говорить, что их использование не есть хороший стиль, зато активно доносят нам про «паттерны», которые являются тривиальными комбинациями обозначенных механизмов. Я считаю, что это такая форма самоиронии.

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

Кстати, я краем уха слышал сказки о том, что когда-то авторы Java хотели выкинуть наследование, заменив его на явные интерфейсы. Вот это был бы Язык, я считаю.

philosophy, idiotic, programming, oo

Previous post Next post
Up