Ну чтож. С цель развеивания мифов и популяризации ФП, так и запишем. СУЩЕСТВУЮТ ДВЕ ПАРАДИГМЫ ПРОГРАММИРОВАНИЯ, КОТОРЫЕ ИМЕЮТ ЧТО-ТО ВРОДЕ ОПРЕДЕЛЕНИЯ. Пусть все услышат и пусть отныне будет так.
Первая — вынести и переиспользовать часть кода классов кодирующих схожие объекты.
Вторая — идея интерфейса — использовать родительский тип как интерфейс к многим разным вариантам имплементации.
Впоследствии идеи разнесли в концепции миксинов и интерфейсов… Или кто там их как называет, поскольку совместно они работали не очень хорошо.
Проблема наследования в том, что оно стимулирует программиста писать довольно большие иерархии классов, тем самым порождая сильную связность, однако идеи положенные в основу наследования по прежнему актуальны. Ждем более удачные реализации.
Но существование какой-то картины в голове мне не ясно. Звучит как магия.
Ну… Это довольно сложная и нестандартная тема…
Я бы сказал, что это и ощущается как магия. Если мы заглянем немного дальше постановки вопроса постижения принципа как культурного явления, мы обнаружим, что программист, как впрочем, и любой представитель творческой профессии, достигая определённого уровня понимания принципа внезапно обнаруживает себя в ситуации отсутствия выбора. Он начинает писать код согласно требованиям принципа и ощущению эстетической красоты. И внезапно оказывается, что не он пишет код, но как бы код сам пишет себя… И это реально ощущается так, как будто кто-то надиктовывает текст программы прямо в мозг.
Но эта тема выходит далеко за рамки технической стороны программирования и затрагивает особенности явления искусства в целом.
Принцип кодирует огромное количество информации и не может быть использован вне культурного кода… Это не набор рекомендаций относительно того, как рыть тунель, но скорее эстетическое представление о том, как рыть тунель. А поскольку эстетическое представление неформализуемо, то оно конечно ощущается магией.
Чтобы научить человека принципу, его надо ввести в культуру и оставить его в этой культуре на какое-то время. Потому что культуре нельзя научить. Ее можно только дать впитать, через тексты представителей культуры, через общение с представителями культуры. Чтобы понять принцип, нужно смотреть на то, как представители культуры действуют, на то, как они думают, на то, как они приходят к своим решениям. И это несколько отличается от обучения конкретным методам. Хотя методы, конечно, тоже являются порождениями культуры. Самым, так сказать, явным её слоем.
Но это всё… Немного за границами настоящего обсуждения.
Парадигма и не должна иметь внятного определения, поскольку она принцип… Это способ взгляда на мир. Эта та картина мира, которая существует в голове программиста, когда он пишет код. Это что-то вроде инженерного мировоззрения и явление это даже более культурное, чем техническое.
Строго говоря, есть всего одна парадигма программирования, которая имеет что-то вроде определения — структурное программирование. Структурное программирование оказалось очень простым и очень успешным. Настолько, что теперь мы все им пользуемся, а принцип структурного программирования стал самоочевидным.
Я вообще полагаю, что изучение парадигм программирования — это довольно высокий уровень. парадигмы где-то на одном уровне с шаблонами проектирования, если не выше. На первых этапах обучения кодерскому искусству изучать парадигмы, как мне кажется, скорее вредно, чем полезно. Это как пытаться разобраться с причинами первой мировой войны, не разбираясь в экономике, психологии, социологии и куче других дисциплин… Ну то есть, можно, но толку особого не будет.
Но мы почему-то считаем, что ООП нужно давать студентам…
Ну… Я пожалуй просто сошлюсь на книжку «Исскуство програмирования для Unix», на которую я уже ссылался в этом треде. Там где-то было достаточно подробное рассмотрение того, почему данные требуют внимания больше, чем алгоритмы.
Современное ООП сильно отличается от идей Алана Кея… Да, отличается.
Вообще, изначальная формулировка, данная господином Кеем имеет скорее историческое значение.
И да, наследование оказалось не очень удачной концепцией. Но методология парадигмы ООП не статична. Она развивается и изменяется. Это нормально.
А функциональное программирование не противоречит ООП, как и ООП не противоречит функциональному программированию. Эти концепции ортогональны. Вы можете писать код, руководствуясь принципами ООП и функциональными принципами одновременно. Следование ООП даст вам хорошую декомпозицию, а следование функциональной парадигме математическую строгость и надёжность, повторяемость, простоту тестирования.
Так же как и философские концепции не противоречат друг другу, парадигмы программирования тоже не соперничают. Они зачастую эксплуатируют общие идеи. Было бы странно, если бы ООП не эксплуатировало модульность, придуманную задолго до возникновения программирования, но ООП берет эту модульность и ставит в центр своей методологии.
Парадигмы программирования можно рассмотреть как некое подмножество приёмов программирования в комбинации продуцирующие код с некоторыми свойствами.
Приёмов очень много, а мы хотим получать более менее предсказуемые результаты. Поэтому мы используем парадигмы, зная что следование парадигме наделит результат проектирования — программу предсказуемыми свойствами.
Иногда за обилием теоретических выкладок не видно тех идей, которые лежат в основе того или иного метода.
А идеи как правило очень просты. Однако, поскольку они просты, написано о них немного, а 99 процентов того, что написано, посвящено следствиям, а не причинам. А между тем изучать следствия без понимания причин довольно бесполезно.
Всё ООП состоит в одной единственной идее, или точнее в одной единственной проблеме.
Нам сложно осознать программу целиком. Программа необозрима и разнородна. И тогда мы принимаем естественное решение. Мы начинаем программу дробить на части и работать с частями независимо.
Давайте попробуем декомпозировать всё, что возможно. Давайте будем разбивать задачи на независимые подзадачи, давайте уменьшать количество информации которую необходимо понимать для того, чтобы разобраться в подзадаче. Давайте уменьшать количество связей в системе.
Всё направлено на то, чтобы мозг мог осознать то, что видят в отдельный момент времени глаза программиста.
Отсюда прямо следует идея инкапсуляции объектов, идея интерфейсов, идея переиспользования кода в том числе в виде наследования и все прочие идеи ООП.
Вся мозгодробильная теория — это не о том, что мы делаем, это о том, как мы делаем, а именно как мы реализуем это самое разбиение.
Логично спросить. А почему мы ставим во главу угла декомпозицию данных, а не алгоритмов. Ведь объекты это в первую очередь данные и связанные с ними алгоритмы, а не наоборот (необходимость упаковки данных вместе с методами прямо следует из идеи инкапсуляции)… И опять же, это следствие того, что нам проще работать с данными имеющими действия, чем с действиями имеющими данными.
Люди мыслят объектно и не умеют обмозговывать большое количество объектов в единицу времени. Вот в этом то собственно и суть ООП.
Сложно сказать, но принципы ООП применялись еще во время древних царств задолго до того же Пифагора. Думаю, пальму первенства формализации ООП можно отдать Демокриту с его идеями как прообразами вещей. Ну, а дальше эта концепция проходит через руки того же Платона и попадает к христианским философам. В целом ООП — очень древняя концепция, происходящая из особенностей работы мозга, а именно необходимости к обобщению, поэтому, я бы сказал, что ООП было всегда.
Из этого следует сделать вывод, что создатели языков и библиотек со статическими и номинальными системами типов осознают проблему и стремятся дать средства к её решению. Это великолепно. Однако мы сейчас беседуем об глобальных свойствах вариантов типизаций. Сам факт того, что такие решения, как те, что приведены выше существуют, показывает наличие проблемы. С противоположной же стороны такой проблемы изначально нет.
Насколько это критично в инженерной практике — вопрос ситуативный.
Успех питона, баша, перла, а также текстовых форматов хранения данных (см. Искусство программирования для Unix) в качестве компонентов системной интеграции, показывает, что как минимум в некоторых задачах динамика превосходит статику.
P.S. Я позволю себе процитировать одного хорошего человека: «В инженерном деле нет ничего хуже, чем религия». Так будем же как и всегда помнить об особенностях инструментов и сообразно их применять.
В качестве примера можно привести многие библиотеки обработки данных и целые фреймворки, завязанные на свои внутренние типы. Возьмём хотя бы Qt. Методы Qt принимают QString, QList, QHashTable, хотя по хорошему, они должны принимать не эти типы, а все похожие на… В результате, при интеграции Qt с прочими библиотеками в точках сопряжения появляется довольно много лишних операций преобразования типов.
Так же ведут себя матбиблиотеки, строящие операции над внутренними типами данных, в результате чего использовать в рамках одной задачи несколько разных матбиблиотек становится довольно накладным из-за постоянной смены оболочек. Довольно часто матбиблиотеки предоставляют средства интеграции, но это всё-таки костыль прикрученный сбоку.
То есть, есть проблема в том, что нельзя передать пользовательский тип в библиотечную функцию малой кровью. Нужно или конвертироваться, или наследоваться. В противоположность этому, тот же numpy способен выполнять многие операции над пользовательскими коллекциями без дополнительной обработки.
IHasFoo + IHasBar + IHasBaz — это очень хорошо. Это явное минимальное описание интерфейса. То есть автор подумал об интеграции и даже задокументировал своё решение в код. Проблема PDFDocument в том что автор не подумал об интеграции. И в номинальном и в структурном исполнении можно найти хорошие и плохие примеры. Я же, впрочем, утверждаю, что сделать хорошую интеграцию на явных интерфейсах сложнее чем на неявных. Это требует меньше телодвижений. Но и в том и в другом случае это требует проектирования, конечно.
Но подождите, как пользователь сможет пользоваться неизвестным значением? Интерфейс всё же также задаётся автором библиотеки, но неявно, в виде какой-то функциональности, которую автор реализует.
Разница есть в силу того, что автор, конструируя номинативный интерфейс не всегда делает это оптимальным способом.
Допустим, в библиотеке есть некая функция Library.foo, которая принимает объект Library.IMyInterface.
Автор Library.IMyInterface мог потребовать реализации кучи разных методов, которые на самом деле в функции foo никогда не вызываются. Явное определение интерфейса практически всегда накладывает более строгие условия на получаемый функцией объект чем это необходимо. При динамической/структурной типизации требуется реализовать только то, что реально используется. Формально можно сказать, что для каждого метода, библиотеки, предназначенных для работы с однотипными объектами, требуемые интерфейсы этих объектов будут различны и всегда минимальны в противоположность явному описанию интерфейса.
Номинативны ли классы в Пайтон? вот вопрос. Согласно определению номинативной структуры типов — да, номинативны, ибо два класса одной структуры, имея разное имя тождественными считаться не будут. Но при этом использование этих классов может быть в питоне абсолютно тождественным в силу динамической типизации.
Если мы рассматриваем открытость и стабильность системы типов, следует понимать структурность и номинативность, как структурность и номинативность интерфейсов, а не типов, поскольку открытость и стабильность системы типов завязана на интерфейсы объектов и способность к кооперации между собой.
Номинативность предполагает тождественность по имени, но в контексте интерфейсов, при динамической типизации нас интересует то, подходит ли объект по структуре, а не по имени.
Поэтому, либо классы пайтон надо считать структурными, либо что-то где-то еще не доверчено по терминологии номинальности и структурности.
В целом способность библиотек к кооперации между собой в динамических языках выше именно из-за того, что нет привязки к заранее сконструированным интерфейсам. Интерфейсы задаются неявно в момент использования в пользовательском коде, а не в момент конструирования класса автором библиотеки. То есть если в номинальном варианте интерфейс должен явно и жестко прописываться автором библиотеки, то при использовании структурной типизации интерфейс — штука довольно зыбкая. При структурной типизации автор пользовательского кода может использовать объект совсем не так, как предполагал автор библиотеки, что затруднено в номинальной системе. Или же автор пользовательского кода может наложить более слабые условия, чем улучшить взаимозаменяемость компонент. Этими вещами объясняется открытость.
Собственно, резюмируя мысль, открытость обеспечивается не структурностью типов, а структурностью интерфейсов типов. Языки, которые допускают структурное взаимодействие в интерфейсах показывают более хорошие результаты по интеграции библиотек. (В качестве примера, кстати, можно посмотреть на шаблоны в С++. Хотя система типов в С++ номинальна, шаблонные интерфейсы вполне себе структурны и имеют описанные свойства.)
P.S. Спасибо за статью. Отделение номинативной и структурной типизации от статической и динамической типизации многое ставит на места.
Хм… Разве это помогает получить min за О(1)?
То есть, для стека — да. Мы можем получить min за O(1), но разве это свойство сохранится, когда у нас будет очередь из двух стеков.
А я вам больше скажу… Довольно сложно писать функционально и при этом необъектно.
:-)
Первая — вынести и переиспользовать часть кода классов кодирующих схожие объекты.
Вторая — идея интерфейса — использовать родительский тип как интерфейс к многим разным вариантам имплементации.
Впоследствии идеи разнесли в концепции миксинов и интерфейсов… Или кто там их как называет, поскольку совместно они работали не очень хорошо.
Проблема наследования в том, что оно стимулирует программиста писать довольно большие иерархии классов, тем самым порождая сильную связность, однако идеи положенные в основу наследования по прежнему актуальны. Ждем более удачные реализации.
Но мне кажется сообщество не до конца привыкло к функциональному программированию и некоторые дебаты вокруг него еще продолжаются.
Думаю лет через десять можно будет уже сказать точно.
Ну… Это довольно сложная и нестандартная тема…
Я бы сказал, что это и ощущается как магия. Если мы заглянем немного дальше постановки вопроса постижения принципа как культурного явления, мы обнаружим, что программист, как впрочем, и любой представитель творческой профессии, достигая определённого уровня понимания принципа внезапно обнаруживает себя в ситуации отсутствия выбора. Он начинает писать код согласно требованиям принципа и ощущению эстетической красоты. И внезапно оказывается, что не он пишет код, но как бы код сам пишет себя… И это реально ощущается так, как будто кто-то надиктовывает текст программы прямо в мозг.
Но эта тема выходит далеко за рамки технической стороны программирования и затрагивает особенности явления искусства в целом.
Принцип кодирует огромное количество информации и не может быть использован вне культурного кода… Это не набор рекомендаций относительно того, как рыть тунель, но скорее эстетическое представление о том, как рыть тунель. А поскольку эстетическое представление неформализуемо, то оно конечно ощущается магией.
Чтобы научить человека принципу, его надо ввести в культуру и оставить его в этой культуре на какое-то время. Потому что культуре нельзя научить. Ее можно только дать впитать, через тексты представителей культуры, через общение с представителями культуры. Чтобы понять принцип, нужно смотреть на то, как представители культуры действуют, на то, как они думают, на то, как они приходят к своим решениям. И это несколько отличается от обучения конкретным методам. Хотя методы, конечно, тоже являются порождениями культуры. Самым, так сказать, явным её слоем.
Но это всё… Немного за границами настоящего обсуждения.
Строго говоря, есть всего одна парадигма программирования, которая имеет что-то вроде определения — структурное программирование. Структурное программирование оказалось очень простым и очень успешным. Настолько, что теперь мы все им пользуемся, а принцип структурного программирования стал самоочевидным.
Я вообще полагаю, что изучение парадигм программирования — это довольно высокий уровень. парадигмы где-то на одном уровне с шаблонами проектирования, если не выше. На первых этапах обучения кодерскому искусству изучать парадигмы, как мне кажется, скорее вредно, чем полезно. Это как пытаться разобраться с причинами первой мировой войны, не разбираясь в экономике, психологии, социологии и куче других дисциплин… Ну то есть, можно, но толку особого не будет.
Но мы почему-то считаем, что ООП нужно давать студентам…
Вообще, изначальная формулировка, данная господином Кеем имеет скорее историческое значение.
И да, наследование оказалось не очень удачной концепцией. Но методология парадигмы ООП не статична. Она развивается и изменяется. Это нормально.
А функциональное программирование не противоречит ООП, как и ООП не противоречит функциональному программированию. Эти концепции ортогональны. Вы можете писать код, руководствуясь принципами ООП и функциональными принципами одновременно. Следование ООП даст вам хорошую декомпозицию, а следование функциональной парадигме математическую строгость и надёжность, повторяемость, простоту тестирования.
Так же как и философские концепции не противоречат друг другу, парадигмы программирования тоже не соперничают. Они зачастую эксплуатируют общие идеи. Было бы странно, если бы ООП не эксплуатировало модульность, придуманную задолго до возникновения программирования, но ООП берет эту модульность и ставит в центр своей методологии.
Парадигмы программирования можно рассмотреть как некое подмножество приёмов программирования в комбинации продуцирующие код с некоторыми свойствами.
Приёмов очень много, а мы хотим получать более менее предсказуемые результаты. Поэтому мы используем парадигмы, зная что следование парадигме наделит результат проектирования — программу предсказуемыми свойствами.
А идеи как правило очень просты. Однако, поскольку они просты, написано о них немного, а 99 процентов того, что написано, посвящено следствиям, а не причинам. А между тем изучать следствия без понимания причин довольно бесполезно.
Всё ООП состоит в одной единственной идее, или точнее в одной единственной проблеме.
Нам сложно осознать программу целиком. Программа необозрима и разнородна. И тогда мы принимаем естественное решение. Мы начинаем программу дробить на части и работать с частями независимо.
Давайте попробуем декомпозировать всё, что возможно. Давайте будем разбивать задачи на независимые подзадачи, давайте уменьшать количество информации которую необходимо понимать для того, чтобы разобраться в подзадаче. Давайте уменьшать количество связей в системе.
Всё направлено на то, чтобы мозг мог осознать то, что видят в отдельный момент времени глаза программиста.
Отсюда прямо следует идея инкапсуляции объектов, идея интерфейсов, идея переиспользования кода в том числе в виде наследования и все прочие идеи ООП.
Вся мозгодробильная теория — это не о том, что мы делаем, это о том, как мы делаем, а именно как мы реализуем это самое разбиение.
Логично спросить. А почему мы ставим во главу угла декомпозицию данных, а не алгоритмов. Ведь объекты это в первую очередь данные и связанные с ними алгоритмы, а не наоборот (необходимость упаковки данных вместе с методами прямо следует из идеи инкапсуляции)… И опять же, это следствие того, что нам проще работать с данными имеющими действия, чем с действиями имеющими данными.
Люди мыслят объектно и не умеют обмозговывать большое количество объектов в единицу времени. Вот в этом то собственно и суть ООП.
Насколько это критично в инженерной практике — вопрос ситуативный.
Успех питона, баша, перла, а также текстовых форматов хранения данных (см. Искусство программирования для Unix) в качестве компонентов системной интеграции, показывает, что как минимум в некоторых задачах динамика превосходит статику.
P.S. Я позволю себе процитировать одного хорошего человека: «В инженерном деле нет ничего хуже, чем религия». Так будем же как и всегда помнить об особенностях инструментов и сообразно их применять.
Так же ведут себя матбиблиотеки, строящие операции над внутренними типами данных, в результате чего использовать в рамках одной задачи несколько разных матбиблиотек становится довольно накладным из-за постоянной смены оболочек. Довольно часто матбиблиотеки предоставляют средства интеграции, но это всё-таки костыль прикрученный сбоку.
То есть, есть проблема в том, что нельзя передать пользовательский тип в библиотечную функцию малой кровью. Нужно или конвертироваться, или наследоваться. В противоположность этому, тот же numpy способен выполнять многие операции над пользовательскими коллекциями без дополнительной обработки.
IHasFoo + IHasBar + IHasBaz — это очень хорошо. Это явное минимальное описание интерфейса. То есть автор подумал об интеграции и даже задокументировал своё решение в код. Проблема PDFDocument в том что автор не подумал об интеграции. И в номинальном и в структурном исполнении можно найти хорошие и плохие примеры. Я же, впрочем, утверждаю, что сделать хорошую интеграцию на явных интерфейсах сложнее чем на неявных. Это требует меньше телодвижений. Но и в том и в другом случае это требует проектирования, конечно.
Разница есть в силу того, что автор, конструируя номинативный интерфейс не всегда делает это оптимальным способом.
Допустим, в библиотеке есть некая функция Library.foo, которая принимает объект Library.IMyInterface.
Автор Library.IMyInterface мог потребовать реализации кучи разных методов, которые на самом деле в функции foo никогда не вызываются. Явное определение интерфейса практически всегда накладывает более строгие условия на получаемый функцией объект чем это необходимо. При динамической/структурной типизации требуется реализовать только то, что реально используется. Формально можно сказать, что для каждого метода, библиотеки, предназначенных для работы с однотипными объектами, требуемые интерфейсы этих объектов будут различны и всегда минимальны в противоположность явному описанию интерфейса.
Если мы рассматриваем открытость и стабильность системы типов, следует понимать структурность и номинативность, как структурность и номинативность интерфейсов, а не типов, поскольку открытость и стабильность системы типов завязана на интерфейсы объектов и способность к кооперации между собой.
Номинативность предполагает тождественность по имени, но в контексте интерфейсов, при динамической типизации нас интересует то, подходит ли объект по структуре, а не по имени.
Поэтому, либо классы пайтон надо считать структурными, либо что-то где-то еще не доверчено по терминологии номинальности и структурности.
В целом способность библиотек к кооперации между собой в динамических языках выше именно из-за того, что нет привязки к заранее сконструированным интерфейсам. Интерфейсы задаются неявно в момент использования в пользовательском коде, а не в момент конструирования класса автором библиотеки. То есть если в номинальном варианте интерфейс должен явно и жестко прописываться автором библиотеки, то при использовании структурной типизации интерфейс — штука довольно зыбкая. При структурной типизации автор пользовательского кода может использовать объект совсем не так, как предполагал автор библиотеки, что затруднено в номинальной системе. Или же автор пользовательского кода может наложить более слабые условия, чем улучшить взаимозаменяемость компонент. Этими вещами объясняется открытость.
Собственно, резюмируя мысль, открытость обеспечивается не структурностью типов, а структурностью интерфейсов типов. Языки, которые допускают структурное взаимодействие в интерфейсах показывают более хорошие результаты по интеграции библиотек. (В качестве примера, кстати, можно посмотреть на шаблоны в С++. Хотя система типов в С++ номинальна, шаблонные интерфейсы вполне себе структурны и имеют описанные свойства.)
P.S. Спасибо за статью. Отделение номинативной и структурной типизации от статической и динамической типизации многое ставит на места.
Живой, живой… Я, правда, немного отвлекся на моделирование динамики… Хотя это побочная ветка :)...
Уникальное — читай " не совместимое ни с чем ".
То есть, для стека — да. Мы можем получить min за O(1), но разве это свойство сохранится, когда у нас будет очередь из двух стеков.
Биомеханика — вполне техническая дисциплина. Так что все впорядке.