Объектно-ориентированное программирование в чисто функциональном контексте программирования?
Есть ли какие-либо преимущества использования объектно-ориентированного программирования (ООП) в контексте функционального программирования (ФП)?
Я уже некоторое время использую F# и заметил, что чем больше у меня функций без состояния, тем меньше мне нужно, чтобы они использовались как методы объектов. В частности, есть преимущество в том, чтобы полагаться на вывод типов, чтобы их можно было использовать в максимально возможном количестве ситуаций.
Это не исключает необходимости использования пространств имен некоторой формы, которая ортогональна тому, чтобы быть ООП. Также не рекомендуется использование структур данных. Фактически, реальное использование языков FP сильно зависит от структур данных. Если вы посмотрите на стек F#, реализованный в http://en.wikibooks.org/wiki/F_Sharp_Programming/Advanced_Data_Structures, вы обнаружите, что он не является объектно-ориентированным.
На мой взгляд, ООП тесно связан с наличием методов, которые воздействуют на состояние объекта, главным образом, для его изменения. В чистом контексте FP это не нужно и не желательно.
Практическая причина может заключаться в возможности взаимодействия с кодом ООП, почти так же, как F# работает с .NET. Однако, кроме этого, есть ли причины? И каков опыт в мире Haskell, где программирование - это более чистый FP?
Я буду признателен за любые ссылки на статьи или контрфактические примеры из реальной жизни по этому вопросу.
3 ответа
Разъединение, которое вы видите, не является FP против ООП. В основном речь идет об неизменности и математических формализмах против изменчивости и неформальных подходов.
Во-первых, давайте избавимся от проблемы изменчивости: у вас может быть FP с изменяемостью и ООП с неизменяемостью. Даже более функциональный, чем у вас, Haskell позволяет вам играть с изменяемыми данными все, что вы хотите, вы просто должны четко указать, что является изменяемым и в каком порядке все происходит; и, кроме проблем с эффективностью, почти любой изменяемый объект может создавать и возвращать новый, "обновленный" экземпляр вместо изменения своего собственного внутреннего состояния.
Большая проблема здесь - математические формализмы, в частности интенсивное использование алгебраических типов данных на языке, мало удаленном от лямбда-исчисления. Вы пометили это с помощью Haskell и F#, но понимаете, что это только половина вселенной функционального программирования; Семейство Lisp имеет совершенно другой, гораздо более свободный характер по сравнению с языками в стиле ML. Большинство широко используемых сегодня ОО-систем носят очень неформальный характер - формализмы для ОО существуют, но они явно не вызываются так, как формализмы ФП в языках стиля ML.
Многие из очевидных конфликтов просто исчезнут, если вы удалите несоответствие формализма. Хотите построить гибкую, динамическую, специальную ОО-систему поверх Lisp? Давай, это будет работать просто отлично. Хотите добавить формализованную неизменяемую ОО-систему к языку в стиле ML? Нет проблем, просто не ожидайте, что он будет хорошо работать с.NET или Java.
Теперь вам может быть интересно, каков соответствующий формализм для ООП? Ну, вот изюминка: во многих отношениях она более функционально ориентирована, чем FP в стиле ML! Я вернусь к одной из моих любимых статей о том, что, по-видимому, является ключевым отличием: структурированные данные, такие как алгебраические типы данных в языках стиля ML, обеспечивают конкретное представление данных и возможность определять операции над ними; объекты обеспечивают абстракцию черного ящика над поведением и способностью легко заменять компоненты.
Здесь есть двойственность, которая гораздо глубже, чем просто FP против ООП: она тесно связана с тем, что некоторые теоретики языка программирования называют проблемой выражения: с конкретными данными вы можете легко добавлять новые операции, которые работают с ними, но изменение структуры данных более сложно. С объектами вы можете легко добавлять новые данные (например, новые подклассы), но добавить новые операции сложно (подумайте о добавлении нового абстрактного метода в базовый класс со многими потомками).
Причина, по которой я говорю, что ООП в большей степени ориентирована на функции, заключается в том, что сами функции представляют собой форму поведенческой абстракции. Фактически, вы можете смоделировать структуру в стиле OO в чем-то вроде Haskell, используя записи, содержащие набор функций в качестве объектов, позволяя типу записи быть своего рода "интерфейсом" или "абстрактным базовым классом", а функции, которые создают записи, заменяют конструкторы классов. Таким образом, в этом смысле ОО-языки используют функции более высокого порядка гораздо чаще, чем, скажем, Haskell.
Для примера чего-то вроде этого типа дизайна, на самом деле очень полезного в Haskell, прочитайте исходный код для пакета graphics-drawingcombinators, в частности, то, как он использует непрозрачный тип записи, содержащий функции, и объединяет вещи только с точки зрения их поведение.
РЕДАКТИРОВАТЬ: Несколько последних вещей, которые я забыл упомянуть выше.
Если ОО действительно широко использует функции более высокого порядка, на первый взгляд может показаться, что он вполне естественно вписывается в функциональный язык, такой как Хаскелл. К сожалению, это не совсем так. Это правда, что объекты, как я их описал (см. Статью, упомянутую в ссылке на LtU), подходят просто отлично. на самом деле, результатом является более чистый стиль ОО, чем у большинства языков ОО, поскольку "закрытые члены" представлены значениями, скрытыми замыканием, используемым для создания "объекта", и недоступны ни для чего, кроме одного конкретного экземпляра. Вы не становитесь намного более приватным, чем это!
Что не очень хорошо работает в Хаскеле, так это подтип. И хотя я думаю, что наследование и подтипы слишком часто неправильно используются в ОО-языках, некоторая форма подтипирования весьма полезна для возможности комбинировать объекты гибкими способами. Хаскеллу не хватает врожденного понятия о подтипах, и заменители, сделанные вручную, имеют тенденцию быть чрезвычайно неуклюжими для работы.
Кроме того, большинство ОО-языков со статическими системами типов также создают полный хэш подтипов, поскольку они слишком слабы с заменяемостью и не обеспечивают надлежащей поддержки дисперсии в сигнатурах методов. На самом деле, я думаю, что единственный полноценный ОО-язык, который не испортил его полностью, по крайней мере, я знаю, это Scala (F#, казалось, слишком много уступал.NET, хотя, по крайней мере, я не думаю, что это делает любые новые ошибки). У меня ограниченный опыт работы со многими такими языками, поэтому я определенно могу ошибаться.
На заметке, относящейся к Haskell, его "классы типов" часто выглядят заманчиво для программистов ОО, на что я говорю: не ходите туда. Попытка реализовать ООП таким способом закончится только слезами. Думайте о классах типов как о замене перегруженных функций / операторов, а не ООП.
Что касается Haskell, классы там менее полезны, потому что некоторые функции ОО легче достигаются другими способами.
Инкапсуляция или "сокрытие данных" часто осуществляется через закрытие функций или экзистенциальные типы, а не через закрытые члены. Например, вот тип данных генератора случайных чисел с инкапсулированным состоянием. ГСЧ содержит метод для генерации значений и начальное значение. Поскольку тип 'seed' инкапсулирован, единственное, что вы можете с ним сделать, это передать его методу.
данные RNG a, где RNG:: (seed -> (a, seed)) -> seed -> RNG a
Диспетчеризация динамических методов в контексте параметрического полиморфизма или "универсального программирования" обеспечивается классами типов (которые не являются классами ОО). Класс типов похож на таблицу виртуальных методов класса OO. Тем не менее, нет данных, скрывающих. Классы типов не "принадлежат" к типу данных, как методы класса.
Координата данных = C Int Int Экземпляр Eq Coordinate, где C a b == C d e = a == b && d == e
Диспетчеризация динамических методов в контексте полиморфизма подтипов или "подклассов" - это почти перевод шаблона класса в Haskell с использованием записей и функций.
- "Абстрактный базовый класс" с двумя "виртуальными методами" Объект данных = объект { draw:: Image -> IO (), перевести:: Coord -> Object } - "Конструктор подкласса" радиус центра круга = объект draw_circle translate_circle где - "методы подкласса" translate_circle радиус центра смещение = круг (центр + смещение) радиус изображение радиуса центра круга draw_circle = ...
Я думаю, что есть несколько способов понять, что означает ООП. Для меня речь идет не об инкапсуляции изменяемого состояния, а скорее об организации и структурировании программ. Этот аспект ООП может прекрасно использоваться в сочетании с концепциями FP.
Я считаю, что смешивание двух понятий в F# - очень полезный подход - вы можете связать неизменяемое состояние с операциями, работающими над этим состоянием. Вы получите приятные возможности "точечного" завершения для идентификаторов, возможность простого использования кода F# из C# и т. Д., Но вы все равно сможете сделать свой код совершенно функциональным. Например, вы можете написать что-то вроде:
type GameWorld(characters) =
let calculateSomething character =
// ...
member x.Tick() =
let newCharacters = characters |> Seq.map calculateSomething
GameWorld(newCharacters)
В начале люди обычно не объявляют типы в F# - вы можете начать просто с написания функций, а затем развить свой код для их использования (когда вы лучше понимаете предметную область и знаете, как лучше структурировать код). Приведенный выше пример:
- Все еще чисто функционально (состояние - список символов, и оно не видоизменено)
- Он является объектно-ориентированным - единственное необычное, что все методы возвращают новый экземпляр "мира"