ФП vs ООП

Автор оригинала: Robert Martin
  • Перевод

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



За последние несколько лет мне неоднократно доводилось программировать в паре с людьми, изучающими Функциональное Программирование, которые были предвзято настроены по отношению к ООП. Обычно это выражалось в формe утверждений типа: “Ну это слишком похоже на что-то объектное.”


Я думаю это происходит из убеждения, что ФП и ООП взаимно исключают друг друга. Многие похоже думают, что если программа функциональная, то она не объектно ориентированная. Полагаю, формирование такого мнения — логичное следствие изучения чего-то нового.


Когда мы берёмся за новую технику, мы часто начинаем избегать старых техник, которые использовали раньше. Это естественно, потому что мы верим, что новая техника “лучше” и следовательно старая техника наверное “хуже”.


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


Что такое ООП?


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


Посмотрите на два выражения:


1: f(o); 2: o.f();


В чём разница?


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


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


С другой стороны, когда мы видим выражение 2. мы видим объект с именем o которому посылают сообщение с именем f. Мы ожидаем, что могут быть другие виды объектов, котоые принимают сообщение f и поэтому мы не знаем, какого конкретно поведения ожидать от f после вызова. Поведение зависит от типа o. то есть f — полиморфен.


Вот этот факт, что мы ожидаем от методов полиморфного поведения — суть объектно ориентированного программирования. Это редукционистское определение и это свойство неустранимо из ООП. ООП без полиморфизма это не ООП. Все другие свойства ООП, такие как инкапсуляция данных и методы привязанные к этим данным и даже наследование имеют больше отношения к выражению 1. чем к выражению 2.


Программисты, использующие Си и Паскаль (и до некоторой степени даже Фортран и Кобол) всегда создавали системы инкапсулированных функций и структур. Чтобы создать такие структуры даже не нужен объектно ориентированный язык программирования. Инкапсуляция и даже простое наследование в таких языках очевидны и естественны. (В Си и Паскале более естественно, чем в других)


Поэтому то, что действительно отличает ООП программы от не ООП программ это полиморфизм.


Возможно вы захотите возразить, что полифорфизм можно сделать просто используя внутри f switch или длинные цепочки if/else. Это правда, поэтому мне нужно задать для ООП ещё одно ограничение.


Использование полиморфизма не должно создавать зависимости вызывающего от вызываемого.


Чтобы это объяснить, давайте ещё раз посмотрим на выражения. Выражение 1: f(o), похоже зависит от функции f на уровне исходного кода. Мы делаем такой вывод потому что мы также предполагаем, что f только одна и что поэтому вызывающий должен знать о вызываемом.


Однако, когда мы смотрим на Выражение 2. o.f() мы предполагаем что-то другое. Мы знаем, что может быть много реализаций f и мы не знаем какая из этих функций f будет вызвана на самом деле. Следовательно исходный код, содержащий выражение 2 не зависит от вызываемой функции на уровне исходного кода.


Если конкретнее, то это означает, что модули (файлы с исходным кодом), которые содержат полиморфные вызовы функций не должны ссылаться на модули (файлы с исходным кодом), которые содержат реализацию этих функций. Не может быть никаких include или use или require или каких-то других ключевых слов, которые создают зависимость одних файлов с исходным кодом от других.


Итак, наше редукционистское определение ООП это:


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

Что такое ФП?


И опять я буду использовать редукционистский подход. У ФП есть богатые традиции и история, корни которых глубже, чем само программирование. Существуют принципы, техники, теоремы, философии и концепции, которыми пронизана эта парадигма. Я всё это проигнорирую и перейду сразу к самой сути, к неотъемлемому свойству которое отделяет ФП от других стилей. Вот оно:


f(a) == f(b) если a == b.


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


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


У этого есть и более глубокое следствие — ни одно именованное значение нельзя менять. То есть оператора присваивания нет.


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


Фокус, конечно, в рекурсии. Рассмотрим функцию которая принимает структуру с состоянием в качестве аргумента. Этот аргумент состоит из всей информации о состоянии, которая нужна функции для работы. Когда работа окончена, функция создаёт новую структуру с состоянием, содержимое которой отличается от предыдущей. И последним действием функция вызывает саму себя с новой структурой в качестве аргумента.


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


Итак, редукционистское определение функционального программирования:


Референциальная Прозрачность — переприсваивать значения нельзя.

ФП против ООП


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


Ясно, что два редукционистских определения, которые я выбрал, совершенно ортогональны. Полиморфизм и Референциальная Прозрачность не имеют никакого отношения друг к другу. Они никак не пересекаются.


Но ортогональность не подразумевает взаимного исключения (спросите Джеймса Клерка Максвелла). Вполне можно создать систему, которая использует и динамический полиморфизм и референциальную прозрачность. Это не только возможно, это правильно и хорошо!


Почему эта комбинация хороша? По точно тем же причинам, что оба её компонента! Системы построенные на динамическом полиморфизме хороши, потому что они обладают низкой связностью. Зависимости можно инвертировать и расположить по разные стороны архитектурных границ. Эти системы можно тестировать используя Моки и Фейки и другие виды Тестовых Дублей. Модули можно модифицировать не внося изменения в другие модули. Поэтому такие системы проще изменять и улучшать.


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


Главная мысль тут такая:


Нет никакого холивара ФП против ООП

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


[1] Так как мы используем машины с архитектурой Фон Неймана мы предполагаем, что в них есть ячейки памяти, состояние которых на самом деле изменяется. В механизме рекурсии, который я описал, оптимизация хвостовой рекурсии не даст создавать новые стекфреймы и будет использоваться первоначальный стекфрейм. Но это нарушение референциальной прозрачности (обычно) скрыто от программиста и ни на что не влияет.

Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 126

    +1
    Мне кажется или полиморфизм в ООП (ad-hoc) вы представили в неком странном виде… в виде того, что у разных обьектов метод с одним и тем же названием — Полиморфизм…
    хм… довольно странная трактовка

    НО! одинаковые методы у разных обьектов не гарантируют полиморфное поведение! Это должен быть один интерфейс, то есть все поведение должно происходить в рамках одной абстракции (либо интерфейс как конструкция, либо наследники c переопределением методов родителя)
    Именно в этом же суть — один метод в рамках одной абстракции?
      0
      Мне кажется или полиморфизм вы представили в неком странном виде… в виде того, что у разных обьектов метод с одним и тем же названием — Полиморфизм…
      Именно в этом суть — один метод в рамках одной абстракции

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

        +2

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

          –1
          На таком уровне и названий никаких нет.

          Ну в статье написано, что объекту передаётся сообщение с именем f, значит название таки есть ))


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

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

      +3
      что хотя ООП и ФП ортогональны, это не взаимно исключающие понятия

      Вы точно понимаете, что такое ортогональность, чтобы употреблять этот термин?

        0

        Я, конечно, понимаю что это такое, но на всякий случай уточню, что термин употребляю не я, а Дяд Боб )))

          +1

          Тем не менее вы повторяете эти глупости за ним.

            +2

            Раскройте, пожалуйста, свою мысль. Почему ООП и ФП не ортогональны?

              +5

              Они вполне себе ортогональны.
              Ортогональность — независимость.
              Взаимное исключение — частный случай взаимной зависимости.
              Взаимоисключающая отогональность — оксюморон.

                0
                Взаимоисключающая отогональность — оксюморон.

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

        0
        Это потому что мы привыкли к тому, что для выражения 2. неявно подразумевается особая семантика поведения, которой нет у выражения 1. Эта особая семантика поведения — полиморфизм.

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

          0
          Вообще-то полиморфизм есть в обоих случаях.

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


          Языки, в которых полиморфизм будет в обоих случаях, как вы справедливо заметили, есть, но в статье речь не о них. Если я правильно понимаю Мартина, то по его мнению мультиметоды — приём из мира ООП и, соответственно, в языках, где они поддерживаются и то и другой код будет объектно-ориентированным.

            0
            Это потому что в мейнстримных языках динамического подбора реализации функции на основании переданных аргументов пока нет.

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

              0

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

                0
                Я уже не раз пытался понять «что же такое ООП» и не смог найти того кирпичика который является его основой.
                Он раскрывает эту тему достаточно доходчиво и с обзором истории в «Clean Architecture: A Craftsman’s Guide to Software Structure and Design», 2017
                  0

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

                    +1

                    Данные относительно функции всегда такие) Просто в ООП языках есть неявная конвенция передавать this первым аргументом. В расте это собственно и вынесли на уровень самого языка, и всем очевидно, что 1.into() и Into::into(1) это совершенно одно и то же.

                      0

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

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

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

                            +1
                            Очевидно же..
                            Википедия — источник, конечно, так себе… ну да ладно, в каком именно предложении вам очевидно?

                            Автору термина «OOP», Alan Kay, например, это не очевидно (или, даже, очевидно не это): «OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.»
                              +1
                              Автору термина «OOP», Alan Kay, например, это не очевидно

                              Но всем очевидно, что сейчас под ООП имеют в виду не то, что имел в виду Алан Кей )))

                                +1
                                Но всем очевидно, что сейчас под ООП имеют в виду не то, что имел в виду Алан Кей
                                Так всем очевидно, или Вам? Вы говорите от лица всех? Интересно…
                                  –1
                                  Так всем очевидно, или Вам?

                                  Мне кажется, большинству, и в первую очередь Алану Кею.


                                  Вы говоритот е от лица всех? Интересно…

                                  Ну, если вы со мной не согласны, то точно не от лица всех )). Но сейчас мало кто не согласится с тем, что Java — объектно-ориентированный язык или с тем, что С++ поддерживает ООП. Но со слов Алана Кея, ООП систему на можно построить только на Лиспе или Смоллтоке.

                                    +1
                                    Позвольте вернуться к началу разговора. Когда вы говорите:
                                    ООП говорит нам — лучше да, чем нет, но необязательно.
                                    то вы посягаете на первоисточность. Это немного не то, чтобы:
                                    кажется, большинству
                                    или
                                    мало кто не согласится с тем

                                    Что говорит нам ООП устами автора этого термина — я вам озвучил.
                                      –1
                                      то вы посягаете на первоисточность

                                      Нет, только на текущее значение аббревиатуры ООП. Слова ведь означают то, что договорились иметь в виду те, кто их использует ))


                                      Что говорит нам ООП устами автора этого термина — я вам озвучил.

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

                                        +1
                                        Вы уверены в том, что значение ООП, заложенное автором этого термина, не реализуется ни одним современным языком программирования? Интересно…

                                        Термины нередко имеют совсем не то значение
                                        Тут многое зависит не от самого термина, а от субъекта восприятия. Раз уж Вы убеждены в том, что Ваше мнение очевидно всем, то невольно напрашивается вопрос — чем Вы тогда здесь сейчас занимаетесь?

                                        P.S.: просто хотел напомнить, что мы обсуждаем не значение термина ООП в понимании тех, кто не знаком с его сутью и историей, а:
                                        должны ли данные быть скрыты для поддержания парадигмы
                                          0
                                          Вы уверены в том, что значение ООП, заложенное автором этого термина, не реализуется ни одним современным языком программирования?

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


                                          Тут многое зависит не от самого термина, а от субъекта восприятия.

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


                                          Раз уж Вы убеждены в том, что Ваше мнение очевидно всем, то невольно напрашивается вопрос — чем Вы тогда здесь сейчас занимаетесь?

                                          Объясняю вам, почему я считаю, что моё мнение очевидно всем )). Хотя, конечно ещё раз отмечу, что если вы со мной не согласны, то явно моё мнение очевидно не всем, а только большинству.


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

                                          Да, совершенно согласен. Я просто хотел сказать, что использовать определение Алана Кея для доказательства своей позиции не совсем корректно, потому что сейчас под ООП имеют в виду не то, что имел в виду Алан Кей

                                            0
                                            что использовать определение Алана Кея
                                            Вы все время уходите в сторону, и путаете «определение» с конкретным формулированием отношения ООП к сокрытию данных, данное автором этого термина. Еще раз напомню — речь идет о сокрытии данных.

                                            для доказательства своей позиции
                                            Процитируете «мою позицию»?

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

                                              Не путаю. Мнение Алана Кея об отношении ООП к сокрытию данных содержится в процитированном вами определении ООП.


                                              Еще раз напомню — речь идет о сокрытии данных.

                                              Да, я помню.


                                              Процитируете «мою позицию»?

                                              Не могу, вы её не озвучили. Думаю, сейчас хороший момент это сделать. У меня из ваших комментариев создаётся впечатление, что ваша позиция в том, что утверждение об отсутствии необходимости скрывать данные в ООП — неверно. Однако, какой бы не была ваша позиция относительно сокрытия данных в ООП, использовать мнение Алана Кея для её обоснования, по той причине, что Алан Кей — автор термина, не совсем корректно.


                                              признав в первом утверждении существование современных «чистых» (в концепции Alan Kay) ООП-языков, вы автоматически оспорили свое второе утверждение.

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


                                              Значит, все-таки не все так считают.

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


                                              Вообще, это плохая привычка говорить от лица всех.

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

                                                0
                                                Ладно, давайте с другой стороны. Ок, Alan Kay — личность известная далеко не всем начинающим программистам, обратимся к (ну, не знаю, к первому взятому наугад) авторитету современности, к автору бестселлера «Code Complete» 2-е изд. Steve McConnell:

                                                «Don't expose member data in public. Exposing member data is a violation of encapsulation and limits your control over the abstraction.» (далее идут отсылки источникам такой формулировки и примеры).

                                                Я не заостряюсь на том, что инкапсуляция — это принцип, и даже отличительная особенность OOP, т.е. без инкапсуляции ООП теряет свои отличительные признаки.

                                                Если верить приводимой вами же Википедии, то вы пришли к "распространённому заблуждению — рассмотрению инкапсуляции неотрывно от сокрытия.". Там же: «некоторые языки (например, Smalltalk, Python) реализуют инкапсуляцию в полной мере, но не предусматривают возможности скрытия в принципе.»

                                                Что такое «Encapsulation»? Смотрим у МакКоннела же:

                                                «Encapsulation picks up where abstraction leaves off. Abstraction says, „You're allowed to look at an object at a high level of detail.“ Encapsulation says, „Furthermore, you aren't allowed to look at an object at any other level of detail.“»

                                                Здесь речь идет об управлении сложностью, но очевидно, что «Encapsulation» неразрывно связана с «Abstraction».

                                                Что такое «Abstraction»? Это то, что обуславливает само существование класса как Abstract Data Type (ADT).

                                                Итак, вот я посмотрел внимательно Вашу Википедию, и самый популярный/авторитетный источник информации. И нигде не обнаружил, что
                                                должны ли данные быть скрыты для поддержания парадигмы?… ООП говорит нам — лучше да, чем нет, но необязательно.

                                                Более того, никаких расхождений с Alan Kay я не обнаружил.

                                                Когда мы говорим об ООП как о парадигме, а не о реализации языковых конструкций для сокрытия данных, — то инкапсуляция, как отличительный признак ООП, непосредственно связанный с абстракцией и самим смыслом существования класса/ADT — должна быть.

                                                Давайте для объективности заглянем к другому (тоже взятому наугад) авторитету, известному по принципам GRASP, Craig Larman, «Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development» 3-е изд.

                                                «Encapsulation — A mechanism used to hide the data, internal structure, and implementation details of some element, such as an object or subsystem. All interaction with an object is through a public interface of operations.»

                                                Интересно, и что же такое «интерфейс»?

                                                «Interface — A set of signatures of public operations.»

                                                О как… интересно, а «operations» — это, случайно, не «данные»?

                                                «Operation — In the UML, „a specification of a transformation or query that an object may be called to execute“ [RJB99]. An operation has a signature, specified by its name and parameters, and it is invoked via a message. A method is an implementation of an operation with a specific algorithm.» (почти как Alan Kay сказал)

                                                Ну, пожалуй, остановимся на двух. С третьим, я думаю, будет тоже самое.

                                                Рад был бы поверить Вам, что большинство считает также как и Вы, но… похоже, что большинство, включая самую авторитетную его часть, все-таки больше на стороне Alan Kay в этом вопросе.
                                                  +1
                                                  Да, кстати, как же я мог забыть… Жирной точкой в этом вопросе можно считать сайт Ward Cunningham (надеюсь, этот человек в представлениях не нуждается). Будем его считать третьим авторитетом (правильней было бы сказать — отцом целого ряда авторитетов). Много текста, поэтому цитировать не буду. Посмотреть можно здесь. Предвосхищая возражения, сразу прошу обратить внимание на «Last edit May 26, 2013» внизу страницы. Если этого мало — загляните еще и в глоссарий ГОФ, а так же в «Библию» ООП — «Object-Oriented Software Construction» 2nd Edition by Bertrand Meyer. В общем, большинство (по крайней мере, авторитетное большинство) — на стороне Alan Kay в этом вопросе.
                                                    0

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

                                0

                                У него наверняка были наилучшие мотивы, когда он это говорил. Увы, то, что он когда-то определил, превратилось в монстра. Особенно последняя часть, про позднее связывание. Обычная участь распопуляризованной идеи.
                                А лично мне это очевидно, потому что ООП я занимался очень много, очень плотно и на разных ЯП, включая те, что изначально не были для этого предназначены (C, JS). И вот ООП часто отлично применимо в рамках языка, но периодически надо жертвовать инкапсуляцией.
                                Грубо говоря, для ООП достаточно одного полиморфизма, просто это будет уж очень неприглядно с виду.

                                  0
                                  Чем ООП принципиально отличается от процедурного стиля? Умением хранить локальные состояния. А переиспользование кода (наследование) и легкое переключение между функциями (полиморфизм) — это фичи ООП, которые появляются следом.

                                  Хотя в современном мире фичи стали важнее/полезнее сути. Ну значит так и должно быть.
                                    0
                                    Чем ООП принципиально отличается от процедурного стиля? Умением хранить локальные состояния.

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

                                    А переиспользование кода (наследование) и легкое переключение между функциями (полиморфизм) — это фичи ООП, которые появляются следом.

                                    Вот это интересный поворот. А можно ещё раз, как полиморфизм и наследование связано с хранением локального состояния?

                                    Кстати, в некотором виде полиморфизм и наследование уж точно присутствуют и в языке Си.

                                    А вообще, полно людей геттеры+сеттеры генерируют для всех локальных переменных в классах, а во всей из себя «ООП» Джаве это ещё и в компайл-тайм вынести пытаются костылями( projectlombok.org/features/GetterSetter ), и называют это инкапсуляцией. Так что отсылка к мнению некого «большинства», ну, так себе аргумент.

                                    P.s. И я больше за отказ от термина ООП в принципе, то есть прекратить вбивать новичкам в голову про «Три Кита» и всё такое, потому что в текущей ситуации и трактовке вреда от него больше чем пользы. Впрочем, видимо, такое быстро не произойдёт.
                                      0
                                      Локальное состояние легко реализуется даже «процедурном» Си. Переменные объявленные в определенном файле будут скрыты и недоступны изначально, если вы сами не сделаете их «публичными» через указание в заголовочном файле.
                                      Да, так тоже можно реализовывать ОО-подход.

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

                                      А вообще, полно людей геттеры+сеттеры генерируют для всех локальных переменных в классах, а во всей из себя «ООП» Джаве это ещё и в компайл-тайм вынести пытаются костылями( projectlombok.org/features/GetterSetter ), и называют это инкапсуляцией. Так что отсылка к мнению некого «большинства», ну, так себе аргумент.
                                      Мне кажется Java-C#-C++ задизайнили в этом смысле не очень как-то (то-ли не было понимания что такое ООП, то-ли оно не очень-то было и нужно), поэтому правильное ООП в них применять неудобно (в отличии от Smalltalk-а) и, в результате, оно выродилось в полу-ООП. Ну и ладно, значит для большинства задач это подходит (и мы видим, что новые ЯП не очень и стремятся наладить обмен сообщениями между объектами, а юзают get/set). Поэтому я и не спорю с общепринятыми понятиями, просто уточняю их в своей речи.

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

                                        А что такое ОО-конструкции?

                                        Мне кажется Java-C#-C++ задизайнили в этом смысле не очень как-то (то-ли не было понимания что такое ООП, то-ли оно не очень-то было и нужно)

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

                                        ну и ладно, значит для большинства задач это подходит

                                        Можно формализовать что значит «подходит»? Так то и Си подходит, можно сказать, для всего.
                                        Но не факт что подходит больше чем что-то другое и эффективнее других инструментов.

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

                                        и мы видим, что новые ЯП не очень и стремятся наладить обмен сообщениями между объектами, а юзают get/set

                                        Странно вы трактовали мою фразу… Сразу сделаю сноску, что это не языки юзают get/set а конкретные программисты.
                                        А get/set это процедурное программирование в чистом виде(есть исключения не значимые в рамках текущей дискуссии). И если исходить упоминавшегося тезиса что популярные и продвигаемые сообществом инструменты лучше непопулярных, ООП/ФП не нужно и всем хватит процедурного кода.

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

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

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

                                        Так я и считаю.
                                          –1
                                          А что такое ОО-конструкции?
                                          Объекты/классы. Хотя классы уже ближе к наследованию.

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

                                          Кстати, во многом хайп вокруг ФП связано с недостатками именно полу-ООП подхода.

                                          Можно формализовать что значит «подходит»? Так то и Си подходит, можно сказать, для всего. Но не факт что подходит больше чем что-то другое и эффективнее других инструментов.

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

                                          не языки юзают get/set а конкретные программисты
                                          Претензия к ЯП может быть только если они делают использование get/set легче, чем полное инкапсулирование.

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

                                            Классы могут быть без наследования, а наследование может быть без классов )
                                            И Объект может быть без класса.
                                            Ну то такое, эти концепты уже и так по другому называются.
                                            Скрин из презентации Кея, например


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

                                            David West вещает о том же примерно( youtube.com/watch?v=RdE-d_EhzmA&list=LLd6OFj5xQf9ZhwBb4EVbdSw&index=58& )

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

                                            Да, и сам Кей в письмах писал что ST стал чем-то «Что нужно изучать». Так не только с ООП, так происходит с очень многими популярными идеями, они упрощаются до такой степени что ничем не отличаются от того что было до этого и натягиваются(название) на уже существующие решения.
                                            Аля «Мы теперь Agile, поэтому будем делать больше митингов», а таски в спринт все-равно менеджер какой-нибудь накидывает единолично.

                                            А вот если бы в Smalltalkе не было непривычных (да и ненужных) вещей, то возможно, что хотя бы Джаву задизайнили в правильном ООП, который был бы легок в использовании, безопасен и не вызывал бы ненужной когнитивной нагрузки.

                                            Кроме как названиями некоторых штук(названиями а не функционалом), Java не особо чем на ST похожа. Это скорее «better c/c++» + крутая фича в виде кроссплатформеннсоти за счёт JVM (Ну девиз их там, типа «write once run everywhere»)

                                            В общем получается, что Алан Кэй и «раскачал» ООП, и он же примешал к нему излишества, которые настолько отождествились со смыслом ООП, что оно в страхе свернуло не туда…

                                            Там были проблемы с лицензированием, о котором syn и xerox не смогли договориться, из-за чего Sun начали пилить свой язык(Опять же — youtube.com/watch?v=RdE-d_EhzmA&list=LLd6OFj5xQf9ZhwBb4EVbdSw&index=58& ).

                                            Претензия к ЯП может быть только если они делают использование get/set легче, чем полное инкапсулирование.

                                            Мм… Проблема то не в языках. Я больше к тому что и на Java пишут процедурщину, и на всём.
                                            get/set используют потому что на краткосрочной дистанции это проще и быстрее, а чтобы правильно инкапсулировать нужные данные и понять что с чем, собственно, инкапсулировать — думать нужно, а неправильная инкапсуляция/декомпозиция может оказаться совсем не лучше чем процедурный код.

                                            И ещё
                                            а) На уровне кода (программист): здесь, чем больше возможностей предоставляет ЯП, тем лучше.

                                            Ну такое. Важно ещё сколько ограничений предоставляет и возможностей ограничений. Например отсутствие множественного наследования я вряд ли стал бы вписывать в исключении, потому что в каком-нибудь kotlin/C# столько удобных способов реюзать код, что наследование уже и еденичное нужно крайне редко.
                                              0
                                              Согласен. Просто считаю, что в настоящее время хорошего ООП не хватает ничуть не меньше, чем чистых функций.
                                                0
                                                Аля «Мы теперь Agile, поэтому будем делать больше митингов», а таски в спринт все-равно менеджер какой-нибудь накидывает единолично.
                                                Если бы только этим все ограничивалось… Проблема гораздо более глубокая. Кстати, да, есть сходство.
                            0
                            В расте это собственно и вынесли на уровень самого языка, и всем очевидно, что 1.into() и Into::into(1) это совершенно одно и то же.

                            А если into() будет принимать интерфейс, то раст подберёт в рантайме функцию для переданной реализации?

                              0

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

                                0
                                Не понял вопроса.

                                Допустим функция into принимает параметр типа InterfaceType (где InterfaceType — интерфейс). И у нас есть другая функция — caller, которая примает параметр типа InterfaceType по имени a. Реальный тип a прикомпиляции неизвестен. Внутри caller написано Into::into(a). Будут ли вызываться разные функции в зависимости от реального типа a?


                                Динамическая диспетчеризация в расте определенно есть, но её стараются избегать

                                Мартин вот советует наоборот, почаще её использовать ))


                                С тайпклассами и макросами она особо и не нужна.

                                Макросы это насколько я понимаю инструмент времени компиляции. А вот насчёт тайплассов как?

                                  +1
                                  Мартин вот советует наоборот, почаще её использовать ))

                                  С генериками оно особо не нужно, как ниже показано.

                                    0
                                    С генериками оно особо не нужно, как ниже показано.

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

                                      +1

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


                                      Любой виртуальный метод abstract class Bar { virtual void Foo() } можно заменить на void Foo<T>(T self) where T : Bar, и смысл будет ровно тот же, с точностью до приватных переменных и прочей оопшной машинерии.

                                        0
                                        Как это не дают? Тогда почему в дотнете вы можете взять (чужую) сборку с генериком, инстанцировать её для своего типа и всё будет работать?

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


                                        Любой виртуальный метод abstract class Bar { virtual void Foo() } можно заменить на void Foo(T self) where T: Bar

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

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

                                          Деталь реализации шарпа, которую при желании может взять любой язык. Тот же раст в dylib экспортирует даже макросы, хотя казалось бы "компайл тайм сущность".


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

                                          Ну потому что стирать типы не стоит, тогда все будет работать ожидаемым образом. А так даже лучше. можно выбрать поведение снаружи объекта (то есть выбрать "базовое" или "специальное" поведение).


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

                                          0
                                          Любой виртуальный метод abstract class Bar { virtual void Foo() } можно заменить на void Foo<T>(T self) where T : Bar

                                          А с override что делать?

                                            +1

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

                                              0

                                              А дальше-то что?

                                                0

                                                А что дальше? Хотели полиморфизм — получили. Квадраты-треугольники вызывают соответствующие методы, все довольны.

                                                  +1

                                                  Вот есть два класса:


                                                  class Foo {
                                                      public virtual void Baz() { 
                                                          Console.WriteLine("I am Foo");
                                                      }
                                                  }
                                                  
                                                  class Bar : Foo {
                                                      public override void Baz() {
                                                          Console.WriteLine("I am Bar");
                                                      }
                                                  }

                                                  На какой такой хитрый дженерик вы сможете заменить метод Foo?

                                                    0

                                                    на where T : IFoo


                                                    если вам нужна вариативность


                                                    Derive(IFoo) for Either<Foo,Bar>

                                                      0

                                                      Я всё еще не вижу как where T : IFoo поможет переписать метод Baz на дженерики.

                                                        +1
                                                        interface IFoo {
                                                           void Baz();
                                                        }
                                                        
                                                        class Foo : IFoo {
                                                            public void Baz() { 
                                                                Console.WriteLine("I am Foo");
                                                            }
                                                        }
                                                        
                                                        class Bar : IFoo {
                                                            public void Baz() {
                                                                Console.WriteLine("I am Bar");
                                                            }
                                                        }
                                                        
                                                        void UseBaz<T>(T t) where T : IFoo => t.Baz();

                                                        Чем такой вариант не устраивает?

                                                          0

                                                          Тем, что он не демонстрирует вашего исходного утверждения:


                                                          Любой виртуальный метод abstract class Bar { virtual void Foo() } можно заменить на void Foo<T>(T self) where T : Bar

                                                          Я вижу в вашем коде замену базового класса на интерфейс, но я не вижу замены виртуального метода на дженерик.

                                                            +1

                                                            Ок, я неправильно сформулировал.


                                                            Любое использование вида void UseFoo(Bar bar) где abstract class Bar { virtual void Foo() } можно заменить на void UseFoo<T>(T bar) where T : Bar.

                                                              0

                                                              Ок, но таким образом вы не избегаете динамической диспетчеризации...

                                                                0

                                                                Избегаем почти везде по кодовой базе. В редких случаях где нам нужно забыть конкретный тип (например сделать Bar[]) придется сделать так же, как принято в ООП, да, там не избегаем.


                                                                Но на практике 99% интерфейсов имеют единственную реализацию (и тестовые моки), в которых вся эта гибкость скорее выходит боком.

                                                                  0

                                                                  Как же вы её избегаете, если ваш пример выше её использует?

                                                                    0

                                                                    Где? UseBaz генерик и никакой виртуализации не использует, оба метода статические. Интерфейс в данном случае просто маркерный "У типа есть такой-то метод", никакой виртуализации от него не требуется. Могу ровно то же на расте написать, там-то никакого наследования очевидно нет.

                                                                      0

                                                                      От рантайма зависит, но когда я последний раз проверял — я не нашел никакой ощутимой разницы в скорости между дженериком и прямым обращением к базовому классу/интерфейсу.

                                                                        0

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

                                                                          0

                                                                          UseFoo<T>(T foo) where T : IFoo — это ни разу не "видно какой тип приходит". Он такой же неопределенный как был и в классическом варианте с UseFoo(IFoo foo)

                                                                            0

                                                                            Это совсем разные вещи. Это явно видно когда реализации IFoo, например, структуры.


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

                                                                              0

                                                                              Да, для структур это и правда видно. Но тут обсуждаются классы.


                                                                              Получается достаточно равноценным.

                                                                              Чтобы оно было равноценным — оно должно быть альтернативой, а тут никакой альтернативы нет. Метод UseFoo<T> использует И интерфейс с динамической диспетчеризацией, И дженерик одновременно.

                                                                                –1

                                                                                Да не использует он динамическую диспетчеризацию.


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


                                                                                Да, для структур это и правда видно. Но тут обсуждаются классы.

                                                                                Я всегда рассуждаю в терминах типов. Структуры/классы — маргинальный выбор когда нужна та или иная семантика копирования.

                                                                                  –2
                                                                                  Да не использует он динамическую диспетчеризацию.

                                                                                  Тогда он — не альтернатива, очевидно.


                                                                                  К слову, composition over interitance работает через динамическую диспетчеризацию. Без сабтайпинга наследование композицией заменить нельзя.

                                                +1
                                                Наверху код знает какой конкретный тип у объекта.

                                                Но на практике-то не знает.

                                                  0

                                                  У нас как-то получается проектировать, что знает.

                                                    +1

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

                                    0

                                    Не в рантайме, а при компиляции.
                                    В рантайме в общем тоже можно, но тут уже вопрос, есть ли доступ к типаж-объекту. И мало кто таким занимается.

                                      0
                                      Не в рантайме, а при компиляции.

                                      Хотелось бы именно в рантайме


                                      В рантайме в общем тоже можно, но тут уже вопрос, есть ли доступ к типаж-объекту. И мало кто таким занимается.

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

                                        0
                                        Тут же всё равно придётся пересобирать код, если хочется добавить новый объект

                                        Эээмм… А как иначе? Это же не скрипт.
                                        Допустим, можно искрутиться и подключить dll/so, закрыв глаза на отсутствие стабильного ABI, и вынуть типаж-объекты оттуда, не владея конечными типами. Rust это прекрасно съест, пока на ограничении типажа не висит 'static. Показать на примере увы, некак, песочница так не умеет, а тестовый пример годичной давности я давно потер. Допускаю, что у меня был не совсем удачный пример, потому что и хост, и плагин я собирал на одной и той же системе, одним компилятором, с разницей в пару минут, а это тепличные условия.

                                0
                                По крайней мере для ФП он прав, ссылочная прозрачность — единственное, что отличает ФП от остальных парадигм.

                                Это если я на js пишу везде с const то у меня ФП сразу?

                                  +1

                                  в жс const к сожалению совсем не означает того что должен. И два вызова console.log не эквивалентны одному.

                          –3
                          Я всё это проигнорирую и перейду сразу к самой сути, к неотъемлемому свойству которое отделяет ФП от других стилей. Вот оно:

                          f(a) == f(b) если a == b.

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

                          Ерунда какая то несусветная.
                          Пусть f(.) — функция потокового шифрования блоков с гаммированием… а некая f_init() ее инициализация. Естественно f(a)=/=f(b) если a=b хотя это самое настоящее ФП. И это самый простой пример когда поведение определено состоянием.
                            +6
                            Интересное у вас понимание ФП. В ФП слово «функция» используется в математическом смысле — как отображение элементов одного множества на элементы другого множества.

                            Соответственно, никакой «инициализации» не может.
                              –3
                              Вы только что отменили все, что написано в ФП в автоматном стиле. А это пласт софта объемом в пропасть. С вашим подходом проще признать что ФП не существует — ибо функция в вашем понимании слишком абстрактна.
                                +3
                                ФП в автоматном стиле? Можно пример?
                              +2

                              Результат работы f_init должен передаваться в функцию f, которая должна работать детерминировано. Тогда будет настоящее ФП

                                –3
                                Тогда получается что:

                                fp=fopen('blablabla.txt',wb);
                                fwrite('blablabla',9,1,fp);
                                fclose(fp);

                                Где то внутри скрыт счетчик от начала файла, и хотя он и передается в скрытом виде через fp — поведение то у fwrite с математической точки зрения, все равно недетерменировано, да еще и место на диске кончилось давно (в математической абстракции то диск естественно бесконечен) т.е. это «не настоящее ФП». А где тогда существует настоящее ФП? разве, что в таких статьях 'ниочем'…
                                  +12

                                  Вы задали прекрасный вопрос. Без контроля над эффектами нет смысла в какой-то там прозрачности, это совершенно неконструктивное понятие.


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


                                  В хаскеле вы точно так же можете написать что-то вроде


                                  doStuff :: IO ()
                                  doStuff = do
                                    h <- fileOpen "blablabla.txt"
                                    fileWrite h "foobar"
                                    fileClose h

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


                                  Вы в ответ на это можете возразить: «ну так давайте считать наши программы на С такой же чистой последовательностью команд, C — чистый язык!» И вы, самое интересное, будете абсолютно правы! Вы можете считать, что весь ваш сишный код просто живёт в монаде IO. Компилятор её там неявно за вас дописывает.


                                  Какое новое знание, новые гарантии это нам даёт? Никаких. Какой в этом смысл? Никакого.


                                  Смысл появляется тогда, когда у вас существуют функции, которые не живут в IO. Которые гарантированно (компилятором) не могут выдавать команды для работы с файловой системой. Или которые гарантированно (компилятором) не могут менять глобальные переменные. Или даже их читать. Или которые гарантированно (компилятором) не лезут в интернет или в БД.


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

                                    0

                                    А зачем это проверять? Ну вот какая вызывающему коду разница куда я там под капотом обращаюсь?

                                      +11
                                      1. Если вы — библиотека для, не знаю, алгоритмов на графах или left-pad, то работа с сетью подозрительна (ну или потом мы удивляемся, что у нас в репозиториях npm/pip/etc всякие майнеры и вирусня заводится).
                                      2. Если немного подять уровень абстракции, то это средство гарантированного DI. Если вы не можете писать на диск или в БД напрямую, а вынуждены заворачиваться в монаду, то если вы вместо монады берёте монадический класс, то вы нахаляву получаете возможность подменить реализацию работы с БД на тестовую/моковую/etc.
                                      3. Когда вам приходится думать о том, что ваша функция делает и какие у неё эффекты, это на практике приводит к более разумной архитектуре и меньшему количеству лапши в коде.
                                      4. Если вызывающий код знает, что вы — чистая функция, то он может вас выполнять, например, в компилтайме во время тайпчекинга, или же в рантайме закешировать ваш результат без опасений.
                                      5. Вы теперь не можете случайно забыть про те или иные эффекты. Что функция может вернуть null, или что она асинхронная, или тому подобные вещи.

                                      Ну и ещё можно привести кучу причин, но они все будут вариациями на эту тему.

                                        –4
                                        1. Библиотека и функция — немного разные понятия. Удобно было бы выдать права один раз на библиотеку (или колстек), а не на каждую функцию в ней.


                                        2. Есть много разных способов инвертировать зависимости помимо DI с ломанием инкапсуляции.


                                        3. Лапши в коде как раз таки становится больше, так как до каждой функции придётся руками доносить всё, что понадобится ей и всем её зависимостям.


                                        4. Если вызывающий код сам является чистой функцией, то ничего закешировать он и не сможет.) Функция чтения файла в компайл тайме тоже детерминирована, но совсем не чиста.


                                        5. А это-то тут при чём?


                                          +2
                                          Удобно было бы выдать права один раз на библиотеку (или колстек), а не на каждую функцию в ней.

                                          Что значит «выдавать права»? Вы просто пишете тип каждой функции точно так же, как писали раньше (ну или он точно так же выводится).


                                          Кроме того, какие права вы выдадите на библиотеку для парсинга жсона, в которой кроме parseJson :: ByteString -> Maybe JSON есть вспомогательный метод parseJsonFromFile :: FilePath -> IO (Maybe JSON)?


                                          Есть много разных способов инвертировать зависимости помимо DI с ломанием инкапсуляции.

                                          Конечно, есть. Но где здесь ломается инкапсуляция, и какие из них гарантируют проверки компилятором, что вы не делаете чего-либо в обход DI?


                                          Лапши в коде как раз таки становится больше, так как до каждой функции придётся руками доносить всё, что понадобится ей и всем её зависимостям.

                                          Зачем? Просто заворачиваетесь в MonadReader.


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

                                          Это у вас просто язык не ленивый.


                                          А это-то тут при чём?

                                          А это точно те же эффекты.

                                            –1
                                            Вы просто пишете тип каждой функции точно так же, как писали раньше (ну или он точно так же выводится).

                                            Я просто пишу getConfig() и меня не волнует откуда он берётся, из базы или памяти или файла или ещё откуда. Это не моя зона ответственности.


                                            есть вспомогательный метод parseJsonFromFile

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


                                            где здесь ломается инкапсуляция

                                            В этом как бы вся суть DI — выворачивать систему кишками наружу.


                                            Это у вас просто язык не ленивый.

                                            А ленивый язык волшебным образом знает что надо кешировать (и на сколько), а что не надо?


                                            это точно те же эффекты.

                                            Возврат null-а определяет исключительно контрактом. А про асинхронность функции мне тем более знать ничего не надо.

                                              +2
                                              Я просто пишу getConfig() и меня не волнует откуда он берётся, из базы или памяти или файла или ещё откуда. Это не моя зона ответственности.

                                              Замечательно, значит, будет там IO, и всё.


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

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


                                              В этом как бы вся суть DI — выворачивать систему кишками наружу.

                                              Почему? Вы просто декларируете «мне нужен логгер, такое-то конфигурационное окружение и доступ к хттп», и всё.


                                              А ленивый язык волшебным образом знает что надо кешировать (и на сколько), а что не надо?

                                              Почти. IO-вычисления не кешируются, чистые значения могут кешироваться, могут вычисляться несколько раз (если компилятор сочтёт это выгодным), и так далее.


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


                                              А про асинхронность функции мне тем более знать ничего не надо.

                                              Хороший подход к программированию.

                                                0
                                                Хороший подход к программированию.

                                                Это не так.


                                                Честно говоря, мне не интересно всё это обсуждать по десятому кругу.

                                        0
                                        Коду вообще все без разницы, а программисту/человеку — нет.

                                        Ниже вы правильно говорите:
                                        Я просто пишу getConfig() и меня не волнует откуда он берётся, из базы или памяти или файла или ещё откуда. Это не моя зона ответственности.

                                        Однако непонятно почему упустили информацию о гарантиях из комментария выше:
                                        Смысл появляется тогда, когда у вас существуют функции, которые не живут в IO. Которые гарантированно (компилятором) не могут выдавать команды для работы с файловой системой. Или которые гарантированно (компилятором) не могут менять глобальные переменные. Или даже их читать. Или которые гарантированно (компилятором) не лезут в интернет или в БД...

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

                                        P.S. надеюсь тут не будет возражений в духе «читайте контракты».
                                          0

                                          Ок, вот вам аналогия по проще: Когда вы пишете a=1 вы можете быть уверены в какой именно регистр процессора попадёт ваша переменная? Ассемблер вам эти гарантии даёт. Любой язык высокого уровня — нет. Это называется абстракция.

                                            +2
                                            Ассемблер вам эти гарантии даёт.

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


                                            Это называется абстракция.

                                            Ну вот и тут абстракция. Если вам неважно, как именно эта конфигурация получена, то вы просто пишете getConfig :: IO Config, и всё. Или getConfig :: MonadIO m => m Config. Или ваше вычисление оформляете как myComputation :: MonadReader Config m => m ....

                                        0
                                        Смысл появляется тогда, когда у вас существуют функции, которые не живут в IO.

                                        Ну почему же. С IO-устройствами тоже можно обращаться функционально, если наложить ограничение на изменяемость данных, т.е. если от CRUD отбросить Update и Delete. Именно в этом и заключается идея Event Sourcing, со слов автора термина CQRS Greg Young. Там у него есть еще вот такой интересный доклад на тему функциональной обработки данных.
                                          0
                                          если наложить ограничение на изменяемость данных, т.е. если от CRUD отбросить Update и Delete

                                          А извне их кто-то при этом может менять?

                                            0
                                            А извне их кто-то при этом может менять?
                                            Если Вы правильно наложите ограничение — то не может. Можно даже наложить это ограничение технически, используя, например, обособленный сетевой периметр, ACL, DB-триггер и т.п. А лучше даже использовать специализированные хранилища. Хотя, чисто технически, можно подменить даже stack вызова функции, — так что тут все зависит от баланса стоимости затрат на реализацию этого ограничения и выгод от его взлома.
                                          0
                                          Смысл появляется тогда, когда у вас существуют функции, которые не живут в IO. Которые гарантированно (компилятором) не могут выдавать команды для работы с файловой системой. Или которые гарантированно (компилятором) не могут менять глобальные переменные. Или даже их читать.

                                          Хм, а подобные проверки никто не пытался прикрутить к компиляторам mainstream языков? В теории должно быть не очень сложно для Java или C#. А если все-таки очень надо, то помечать аннотацией какой-нибудь. (кажется я знаю, чем займусь этим летом).
                                          Или которые гарантированно (компилятором) не лезут в интернет или в БД.

                                          Такое скорее всего не получится, к сожалению.
                                            0
                                            Хм, а подобные проверки никто не пытался прикрутить к компиляторам mainstream языков?

                                            Пытались прикрутить атрибут pure к C++, получилось не очень.


                                            В теории должно быть не очень сложно для Java или C#.

                                            Для подмножества без дженериков — да, вполне. Просто идёте по дереву вызовов и помечаете соответствующим образом узлы.


                                            Правда, если вы про узел что-то не знаете (например, даже про функцию стандартной библиотеки, пока вы сами её не аннотировали на честном слове), то вам придётся быть весьма пессимистичным.

                                            0
                                            Напомню что у нас проблема вообще то в классификации.
                                            Очевидно, что примеры идентичны.
                                            Вопрос стоит являются ли они ОБА ФП — (заявленный признак бред)
                                            Или оба НЕ ЯВЛЯЮТСЯ (заявленный признак истинна — но тогда чем они являются? ООП что ли?! ).
                                              0
                                              Вы в ответ на это можете возразить: «ну так давайте считать наши программы на С такой же чистой последовательностью команд, C — чистый язык!» И вы, самое интересное, будете абсолютно правы! Вы можете считать, что весь ваш сишный код просто живёт в монаде IO. Компилятор её там неявно за вас дописывает.
                                              Я думаю, что после этого предложения стоило бы дописать:
                                              Впрочем, в вашем сишном коде нет функций, потому что ваш сишный код — экземпляр нефункционального типа IO.

                                              И вывод: ФЯП нужны, поскольку писать без функций, когда в предметной области есть функциональные зависимости, неуютно.
                                              +1
                                              Где то внутри скрыт счетчик от начала файла, и хотя он и передается в скрытом виде через fp — поведение то у fwrite с математической точки зрения, все равно недетерменировано т.е. это «не настоящее ФП»

                                              Да, всё так, это классический пример нарушения чистоты функции.


                                              А где тогда существует настоящее ФП?

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

                                                0
                                                И зачем нужен движок игры — без самой игры и игрока? может быть движок калькулятора без недетерменированого ввода с клавиатуры того что нужно калькулировать какое то применение имеет? Как у вас это работает? движок игры — ФП. а все игры на нем написанные — что тогда?

                                                99% функций чистые 1% грязные — как будем это классифицировать?
                                                  0
                                                  И зачем нужен движок игры — без самой игры и игрока?

                                                  Чтобы встраивать в игру, в которую будет играть игрок ))


                                                  движок игры — ФП. а все игры на нем написанные — что тогда?

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

                                          +1

                                          Естественно есть холивар и противопоставление.


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

                                            0
                                            Естественно есть холивар и противопоставление.

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

                                            Мне кажется, вы сейчас говорите о холиваре между статической типизацией и динамической типизацией. У Мартина есть рассуждения на эту тему и насколько я понял, он считает, что типизация ортогональна что ФП, что ООП.

                                              +1

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

                                                +1
                                                Вы (или я в своём комментарии) сразу постулируете полезность статической типизации как средства для того, чтобы избегать ерунды

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


                                                И получается, что некоторые каноничные ООП-фишки не очень хорошо работают с некоторыми ФП-фишками.

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


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


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

                                                Кстати, если добавить сюда юнит тесты — точка зрения Дяди Боба.

                                                  +4
                                                  Собственно Мартин как раз говорит, что не так уж она и полезна.

                                                  Смотря какая. Кое-где её тяжесь, вербозность или неуклюжесть действительно может не оправдывать даваемые ей гарантии.


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

                                                  Ну, типа того, да.


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


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

                                                  Но декоратор абстрактной фабрики синглтонов тоже дешугарится в функцию, если посмотреть пристально (или почитать несчастного Пирса)!


                                                  Кстати, если добавить сюда юнит тесты — точка зрения Дяди Боба.

                                                  И правда, зачем нам готовый тайпчекер, если можно каждый раз писать (и поддерживать) его частный и кривоватый случай.

                                                    +1
                                                    Наверное, пора написать постик на эту тему [что сама по себе референциальная прозрачность бессмысленна].

                                                    Я думаю надо написать, я вроде таких не читал.


                                                    И правда, зачем нам готовый тайпчекер, если можно каждый раз писать (и поддерживать) его частный и кривоватый случай.

                                                    Готовый тайпчекер слишком жёсткий, если сделать кастомный, то код получится проще и выразительнее

                                            +1
                                            Очередное мнение.
                                              0
                                              Интересный подход, но всё же обычно ООП подразумевает объект и изменяемым состоянием, можно конечно порождать новые объекты, но это будет некая новая разновидность ООП.
                                                0
                                                обычно ООП подразумевает объект и изменяемым состоянием

                                                К этому все привыкли, но в последнее время неизменяемые объекты встречаются в коде всё чаще и чаще.


                                                можно конечно порождать новые объекты, но это будет некая новая разновидность ООП

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

                                                0
                                                Когда мы видим выражение 1. мы видим функцию f, которая вызывается в которую передаётся объект o. При этом подразумевается, что есть только одна функция с именем f

                                                Да ладно? А как же перегрузка функций? И как же статический полиморфизм с выбором реализации на основе типа возвращаемого значения, как это в ФП любят?

                                                  0
                                                  А как же перегрузка функций?

                                                  Дальше из текста статьи понятно, что под функцией с именем f Мартин имеет в виду функцию с именем f, которая принимает параметр o какого-то конкретного типа. Наверное не счёл нужным уточнять.


                                                  И как же статический полиморфизм с выбором реализации на основе типа возвращаемого значения, как это в ФП любят?

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

                                                    0
                                                    Дальше из текста статьи понятно, что под функцией с именем f Мартин имеет в виду функцию с именем f, которая принимает параметр o какого-то конкретного типа

                                                    А как насчёт вот такой функции?


                                                    id :: forall a. a -> a

                                                    Тут forall необязателен, но я его написал чтобы было понятно, что это именно что одна функция, а не семейство функций… Какой конкретный тип параметра она принимает? :-)

                                                      0

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

                                                        0

                                                        Похоже-непохоже, но в статье про ФП пропускать Хаскель всё равно нельзя.


                                                        Да, в Джаве был бы дженерик.

                                                          0

                                                          Тут важный момент, что это магия времени компиляции, а Мартин делает акцент на динамический полиморфизм. Что касается Хаскеля, то у меня сложилось впечатление, что у Мартина ФП автоматически означает Clojure

                                                            0

                                                            Ну вот вам магия рантайма:


                                                            data Foo = forall s. Show s => Foo s
                                                            instance Show Foo where
                                                                show (Foo x) = show x

                                                            Тут функция show (аналог toString) только в рантайме узнает аргумент какого типа ей надо превратить в строку.

                                                              0

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


                                                              На самом деле когда вы пишете data Foo = forall s. Show s => Foo s и когда вы создаёте значение типа Foo из someShowable, то конструктор сохраняет, условно, указатель на это значение (без всяких типов, просто указатель, как void*) и указатель на словарь методов класса Show (т. е., считайте, указатель на функцию, которая принимает тот void* и возвращает строку).

                                                                0

                                                                А это уже детали реализации.

                                                        0

                                                        Она принимает тип, а его тип — Type (или *, или что хотите). Если взять не хаскелевский синтаксис, а нормальный, то получится id : Пa:*. a -> a. Ну или


                                                        > :t id
                                                        Prelude.Basics.id : a -> a
                                                        > :set showimplicits 
                                                        > :t id
                                                        Prelude.Basics.id : {a : Type} -> a -> a

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

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

                                                  Самое читаемое