Объектно-ориентированное программирование в контексте чисто функционального программирования?

Есть ли какие-либо преимущества в использовании объектно-ориентированного программирования (ООП) в контексте функционального программирования (FP)?

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

Это не исключает необходимости в пространствах имен некоторой формы, которая ортогональна ООП. Также не рекомендуется использовать структуры данных. Фактически, реальное использование языков FP сильно зависит от структур данных. Если вы посмотрите на стек F #, реализованный в F Sharp Programming/Advanced Data Structures, вы обнаружите, что он не является объектно-ориентированным.

На мой взгляд, ООП сильно связан с тем, что методы, которые воздействуют на состояние объекта главным образом, мутируют объект. В чистом контексте FP, который не нужен и не нужен.

Практической причиной может быть возможность взаимодействия с кодом ООП, во многом таким же образом, что F # работает с .NET. Кроме этого, есть ли какие-то причины? И каков опыт в мире Haskell, где программирование более чистое FP?

Я буду признателен за любые ссылки на документы или примеры контрафактного реального мира по этому вопросу.

Ответ 1

Отключение, которое вы видите, не относится к FP и OOP. В основном это касается неизменяемости и математических формализмов против изменчивости и неформальных подходов.

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

Большей проблемой здесь являются математические формализмы, в частности интенсивное использование алгебраических типов данных на языке, немного удаленном от лямбда-исчисления. Вы отметили это с помощью Haskell и F #, но понимаете, что только половина функционального программирования юниверса; семейство Lisp имеет совсем другой, гораздо более свободный характер по сравнению с языками в стиле ML. Большинство систем ОО, широко используемых сегодня, очень неформальны по своей природе: формализм существует для ОО, но они не вызывают явно, как формалисты FP находятся в языках стиля ML.

Многие из кажущихся конфликтов просто исчезают, если вы устраняете несоответствие формализма. Хотите создать гибкую, динамичную, ad-hoc OO-систему поверх Lisp? Давай, все будет хорошо. Хотите добавить формализованную, неизменную систему OO на язык ML-стиля? Нет проблем, просто не ожидайте, что он будет хорошо играть с .NET или Java.


Теперь вам может показаться, что это правильный формализм для ООП? Ну, вот ударная линия: во многом это более функционально, чем ML-стиль FP! Я вернусь к одной из моих любимых работ за то, что, по-видимому, является ключевым отличием: структурированные данные, такие как алгебраические типы данных в языках ML-стиля обеспечивают конкретное представление данных и возможность определять на нем операции; объекты обеспечивают абстракцию черного ящика по сравнению с поведением и возможность легко заменять компоненты.

Здесь двойственность, которая идет глубже, чем просто FP против OOP: она тесно связана с тем, что некоторые теоретики языка программирования называют проблемой выражения: С конкретными данными вы можете легко добавлять новые операции, которые с ним работают, но изменить структуру данных сложнее. С объектами вы можете легко добавлять новые данные (например, новые подклассы), но добавлять новые операции сложно (подумайте о добавлении нового абстрактного метода в базовый класс со многими потомками).

Причина, по которой я говорю, что ООП более функционально-ориентированная, состоит в том, что сами функции представляют собой форму поведенческой абстракции. Фактически, вы можете моделировать структуру OO-стиля в чем-то вроде Haskell, используя записи, содержащие кучу функций как объекты, позволяя типу записи быть "интерфейсом" или "абстрактным базовым классом", и иметь функции, которые создают записи, заменяют класса. Поэтому в этом смысле языки OO используют функции более высокого порядка далеко, гораздо чаще, чем, скажем, Haskell.

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


EDIT: Несколько последних вещей, о которых я забыл упомянуть выше.

Если OO действительно широко использует функции более высокого порядка, сначала может показаться, что он должен очень естественно входить в функциональный язык, такой как Haskell. К сожалению, это не совсем так. Это правда, что объекты, как я их описывал (см. Документ, упомянутый в ссылке LtU), вполне соответствуют. на самом деле результат - более чистый стиль OO, чем большинство языков OO, потому что "частные члены" представлены значениями, скрытыми закрытием, используемым для построения "объекта", и недоступны для чего-либо другого, кроме самого конкретного экземпляра. Вы не становитесь более частным, чем это!

Что не очень хорошо работает в Haskell, это подтипирование. И, хотя я считаю, что наследование и подтипирование слишком часто используются неправильно на языках OO, некоторая форма подтипирования весьма полезна для возможности комбинировать объекты гибкими способами. Haskell не обладает неотъемлемым понятием подтипирования, и ручные замены, как правило, чрезвычайно неудобны для работы.

В стороне, большинство языков OO с системами статического типа также делают полный хэш подтипов, слишком слабый с заменяемостью и не обеспечивают надлежащую поддержку для дисперсии в сигнатурах методов. На самом деле, я думаю, что единственный полномасштабный язык OO, который не исчерпал его полностью, по крайней мере, я знаю, это Scala (F #, похоже, слишком много уступок .NET, хотя, по крайней мере, думаю, что он совершает новые ошибки). Однако у меня ограниченный опыт работы со многими такими языками, поэтому я определенно ошибаюсь.

В примечании, характерном для Haskell, его "классы типов" часто выглядят заманчивыми для программистов OO, о которых я говорю: "Не ходите туда". Попытка реализовать ООП таким образом будет только заканчиваться слезами. Подумайте о типах классов в качестве замены перегруженных функций/операторов, а не ООП.

Ответ 2

Что касается Haskell, классы менее полезны там, потому что некоторые функции OO легче достичь другими способами.

Инкапсуляция или "скрытие данных" часто выполняются посредством закрытия функций или экзистенциальных типов, а не частных. Например, здесь приведен тип данных генератора случайных чисел с инкапсулированным состоянием. RNG содержит метод генерации значений и начального значения. Поскольку тип "семя" инкапсулирован, единственное, что вы можете сделать с ним, - передать его методу.

data RNG a where RNG :: (seed -> (a, seed)) -> seed -> RNG a

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

data Coordinate = C Int Int

instance Eq Coordinate where C a b == C d e = a == b && d == e

Отправка динамического метода в контексте политизма подтипов или "подклассификация" - это почти перевод шаблона класса в Haskell с использованием записей и функций.

-- An "abstract base class" with two "virtual methods"
data Object =
  Object
  { draw :: Image -> IO ()
  , translate :: Coord -> Object
  }

-- A "subclass constructor"
circle center radius = Object draw_circle translate_circle
  where
    -- the "subclass methods"
    translate_circle center radius offset = circle (center + offset) radius
    draw_circle center radius image = ...

Ответ 3

Я думаю, что есть несколько способов понять, что означает ООП. Для меня речь идет не о инкапсулировании изменчивого состояния, а о организации и структурировании программ. Этот аспект ООП можно использовать отлично в сочетании с концепциями FP.

Я считаю, что смешивание двух понятий в F # - очень полезный подход - вы можете связать неизменяемое состояние с операциями, работающими над этим состоянием. Вы получите приятные функции "точечного" завершения для идентификаторов, возможность простого использования кода F # с С# и т.д., Но вы все равно можете сделать свой код совершенно функциональным. Например, вы можете написать что-то вроде:

type GameWorld(characters) = 
  let calculateSomething character = 
    // ...
  member x.Tick() = 
    let newCharacters = characters |> Seq.map calculateSomething
    GameWorld(newCharacters)

В начале люди обычно не объявляют типы в F # - вы можете начать с простого написания функций, а затем разработать свой код для их использования (когда вы лучше поймете домен и узнаете, что является лучшим способом структурирования кода), Вышеприведенный пример:

  • Все еще чисто функциональный (состояние - это список символов, и он не мутирован)
  • Объектно-ориентированное - единственная необычная вещь заключается в том, что все методы возвращают новый экземпляр "мира"