Функциональное программирование по сравнению с ООП с классами


32

В последнее время меня интересуют некоторые концепции функционального программирования. Я использовал ООП уже некоторое время. Я вижу, как я могу построить довольно сложное приложение в ООП. Каждый объект будет знать, как делать то, что делает объект. Или что-нибудь, что делает класс родителей. Так что я могу просто сказать, Person().speak()чтобы заставить человека говорить.

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

Таким образом, я могу видеть простые вещи, просто как мне сделать сопоставимые ООП и объекты в функциональном программировании, чтобы я мог модульно структурировать и организовать свою базу кода?

Для справки, мой основной опыт работы с ООП - это Python, PHP и немного C #. Языки, на которые я смотрю, имеют функциональные особенности - Scala и Haskell. Хотя я склоняюсь к Скале.

Базовый пример (Python):

Animal(object):
    def say(self, what):
        print(what)

Dog(Animal):
    def say(self, what):
        super().say('dog barks: {0}'.format(what))

Cat(Animal):
    def say(self, what):
        super().say('cat meows: {0}'.format(what))

dog = Dog()
cat = Cat()
dog.say('ruff')
cat.say('purr')

Scala разработан как OOP + FP, поэтому вам не нужно выбирать
Karthik T

1
Да, я знаю, но я также хочу знать по интеллектуальным причинам. Я не могу найти ничего о эквиваленте объекта в функциональных языках. Что касается scala, я все еще хотел бы знать, когда / где / как я должен использовать функционал поверх oop, но это IMHO - другой вопрос.
Скиф

2
«Особо подчеркивается, что ИМО - это понятие о том, что мы не поддерживаем государство». Это неверное понятие. Это не правда, что FP не использует состояние, скорее FP обрабатывает состояние по-другому (например, монады в Haskell или уникальные типы в Clean).
Джорджио


Ответы:


21

Здесь вы действительно спрашиваете, как сделать полиморфизм в функциональных языках, то есть как создать функции, которые ведут себя по-разному в зависимости от своих аргументов.

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

Функциональные языки в целом предоставляют различные варианты достижения полиморфизма:

  • Что-то вроде мультиметода, который вызывает другую функцию на основе изучения предоставленных аргументов. Это может быть сделано для типа первого аргумента (который фактически равен поведению большинства языков ООП), но также может быть сделано для других атрибутов аргументов.
  • Прототип / объектоподобные структуры данных, которые содержат первоклассные функции в качестве членов . Таким образом, вы можете встроить функцию «сказать» в структуры данных вашей собаки и кошки. Эффективно вы сделали часть кода данных.
  • Сопоставление с образцом - когда логика сопоставления с образцом встроена в определение функции и обеспечивает различное поведение для разных параметров. Обычный в Хаскеле.
  • Ветвление / условия - эквивалент предложений if / else в ООП. Может быть не очень расширяемым, но все же может быть уместным во многих случаях, когда у вас ограниченный набор возможных значений (например, передана ли функция число, строка или ноль?)

В качестве примера приведем реализацию вашей проблемы в Clojure с использованием мультиметодов:

;; define a multimethod, that dispatched on the ":type" keyword
(defmulti say :type)  

;; define specific methods for each possible value of :type. You can add more later
(defmethod say :cat [animal what] (println (str "Car purrs: " what)))
(defmethod say :dog [animal what] (println (str "Dog barks: " what)))
(defmethod say :default [animal what] (println (str "Unknown noise: " what)))

(say {:type :dog} "ruff")
=> Dog barks: ruff

(say {:type :ape} "ook")
=> Unknown noise: ook

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


Не на 100% ясно, но достаточно, чтобы увидеть, куда вы идете. Я мог видеть это как код «животного» в данном файле. Также хороша часть по ветвлению / условиям. Я не рассматривал это как альтернативу if / else.
Скиф

11

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

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

Для меня «прорыв» наступил, когда я решил изучать питон. Я скачал python, затем перешел на домашнюю страницу проекта euler и просто начал делать одну проблему за другой. Python не обязательно является языком FP, и вы, безусловно, можете создавать в нем классы, но по сравнению с C ++ / Java / C # он имеет гораздо больше конструкций FP, поэтому, когда я начал с ним играть, я принял сознательное решение не определить класс, если я не должен был.

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

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

Однако ключ к FP заключается в том, что у вас нет побочных эффектов. Пока вы рассматриваете приложение как простое преобразование данных с определенным набором входов и наборов выходов, вы можете написать код FP, который бы выполнил то, что вам нужно. Очевидно, что не каждое приложение хорошо вписывается в эту форму, но как только вы начнете это делать, вы удивитесь, сколько приложений подойдет. И здесь я думаю, что Python, F # или Scala сияют, потому что они дают вам конструкции FP, но когда вам нужно помнить ваше состояние и «вводить побочные эффекты», вы всегда можете прибегнуть к истинным и проверенным методам ООП.

С тех пор я написал целую кучу кода Python в качестве утилит и других вспомогательных сценариев для внутренней работы, и некоторые из них были значительно расширены, но, помня основные принципы SOLID, большая часть этого кода все еще получалась очень удобной в обслуживании и гибкой. Так же, как в ООП, ваш интерфейс является классом, и вы перемещаете классы по мере рефакторинга и / или добавления функциональности, в FP вы делаете то же самое с функциями.

На прошлой неделе я начал программировать на Java, и с тех пор почти ежедневно мне напоминают, что в ООП я должен реализовывать интерфейсы, объявляя классы с помощью методов, которые переопределяют функции, в некоторых случаях я могу добиться того же в Python, используя простое лямбда-выражение, например, 20-30 строк кода, которое я написал для сканирования каталога, в Python было бы 1-2 строками и без классов.

ФП сами по себе являются языками более высокого уровня. В Python (извините, мой единственный опыт работы с FP) я мог собрать понимание списка в другом понимании списка с добавленными лямбдами и прочим материалом, и все это было бы всего лишь 3-4 строками кода. В C ++ я мог бы абсолютно точно выполнить то же самое, но поскольку C ++ является более низким уровнем, мне пришлось бы писать гораздо больше кода, чем 3-4 строки, и по мере увеличения количества строк мое обучение SRP начиналось, и я начинал думать о том, как разделить код на более мелкие части (то есть, больше функций). Но в интересах удобства сопровождения и скрытия деталей реализации я бы поместил все эти функции в один класс и сделал их приватными. И вот, у вас это есть ... Я только что создал класс, тогда как в python я написал бы «return (.... lambda x: .. ....)»


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

Отличный ответ. Говорит со всеми, кто стоит у бассейна, но не уверен, стоит ли им опускать носок в воду
Robben_Ford_Fan_boy

И Руби ... Одна из его философий проектирования сосредоточена на методах, принимающих блок кода в качестве аргумента, обычно необязательный. А учитывая чистый синтаксис, думать и кодировать таким образом легко. Трудно так думать и сочинять в C #. Функциональность C # является многословной и дезориентирующей, она воспринимается как язык. Мне нравится, что Ruby помогал мыслить функционально легче, видеть потенциал в моем постоянном C # блоке мысли. В конечном счете я вижу функционал и ОО как взаимодополняющий; Я бы сказал, что Руби, безусловно, так думает.
Радар Боб

8

В Хаскеле самый близкий у вас "класс". Этот класс, хотя и не такой, как класс в Java и C ++ , будет работать для того, что вы хотите в этом случае.

В вашем случае так будет выглядеть ваш код.

класс животных где 
сказать :: Строка -> звук 

Тогда вы можете иметь отдельные типы данных, адаптирующие эти методы.

экземпляр Animal Dog где
сказать s = "кора" ++ s 

РЕДАКТИРОВАТЬ: - Прежде чем вы можете специализироваться, скажем, для собаки, вы должны сказать системе, что собака является животным.

данные собаки = \ - что-то здесь - \ (производное животное)

РЕДАКТИРОВАТЬ: - Для Wilq.
Теперь, если вы хотите использовать say в функции say foo, вам придется сказать haskell, что foo может работать только с Animal.

foo :: (Animal a) => a -> String -> String
foo a str = скажи ул 

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

main = do 
let d = собака (\ - cstr параметры - \)
    с = кошка  
в шоу $ foo d "Hello World"

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


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

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

1
Как функция say узнает, что вы вызываете ее для Dog, если она принимает только String? И не является ли «производным» только для некоторых встроенных классов?
WilQu

6

Функциональные языки используют 2 конструкции для достижения полиморфизма:

  • Функции первого порядка
  • Дженерики

Создание полиморфного кода с этим совершенно отличается от того, как ООП использует наследование и виртуальные методы. Хотя оба из них могут быть доступны на вашем любимом языке ООП (например, C #), большинство функциональных языков (например, Haskell) поддерживают его до одиннадцати. Редко функционировать, чтобы быть не универсальным, и большинство функций имеют функции в качестве параметров.

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


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

4
полиморфизм - это только один аспект ООП, и я думаю, что не тот, о котором ОП действительно спрашивает.
Док Браун

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

1
@ErikReppen Если «применить интерфейс» не требуется часто, значит, вы не выполняете ООП. Также на Haskell есть модули.
Эйфорическая

1
Вам не всегда нужен интерфейс. Но они очень полезны, когда они вам нужны. И ИМО еще одна важная часть ООП. Что касается модулей в Haskell, я думаю, что это, вероятно, ближе всего к ООП для функциональных языков, что касается организации кода. По крайней мере, из того, что я прочитал до сих пор. Я знаю, что они все еще очень разные.
Скиф

0

это действительно зависит от того, чего вы хотите достичь.

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

def bark(what):
    print "barks: {0}".format(what) 

def meow(what):
    print "meows: {0}".format(what)

def climb(how):
    print "climbs: {0}".format(how)

if __name__ == "__main__":
    animals = {'dog': {'say': bark},
               'cat': {'say': meow,
                       'climb': climb}}
    animals['dog']['say']("ruff")
    animals['cat']['say']("purr")
    animals['cat']['climb']("well")

заметьте, однако, что (а) нет «экземпляров» собаки или кошки и (б) вам придется отслеживать «тип» ваших объектов самостоятельно.

как, например: pets = [['martin','dog','grrrh'], ['martha', 'cat', 'zzzz']]. тогда вы могли бы сделать понимание списка, как[animals[pet[1]]['say'](pet[2]) for pet in pets]


0

Языки OO могут использоваться вместо языков низкого уровня, иногда для непосредственного взаимодействия с машиной. C ++ Конечно, но даже для C # есть адаптеры и тому подобное. Хотя написание кода для управления механическими деталями и минимального контроля над памятью лучше поддерживать как можно ближе к низкому уровню. Но если этот вопрос относится к текущему объектно-ориентированному программному обеспечению, такому как Line Of Business, веб-приложениям, IOT, веб-службам и большинству массово используемых приложений, то ...

Ответьте, если применимо

Читатели могут попробовать работать с сервис-ориентированной архитектурой (SOA). То есть DDD, N-Layered, N-Tiered, Hexagonal, что угодно. Я не видел, чтобы крупное бизнес-приложение эффективно использовало «Традиционные» ОО (Active-Record или Rich-Models), как это было описано в 70-х и 80-х годах в последнее десятилетие. (См. Примечание 1)

Ошибка не в ОП, но есть несколько проблем с вопросом.

  1. Приведенный вами пример просто демонстрирует полиморфизм, это не рабочий код. Иногда подобные примеры воспринимаются буквально.

  2. В FP и SOA Данные отделены от бизнес-логики. То есть данные и логика не идут вместе. Логика входит в службы, а данные (доменные модели) не имеют полиморфного поведения (см. Примечание 2).

  3. Услуги и функции могут быть полиморфными. В FP вы часто передаете функции в качестве параметров другим функциям вместо значений. Вы можете сделать то же самое в OO Languages ​​с такими типами, как Callable или Func, но он не работает безудержно (см. Примечание 3). В FP и SOA ваши Модели не являются Полиморфными, только ваши Услуги / Функции. (См. Примечание 4)

  4. В этом примере плохой случай жесткого кодирования. Я говорю не только о красной строке "собака лает". Я также говорю о самих CatModel и DogModel. Что происходит, когда вы хотите добавить овец? Вы должны пойти в свой код и создать новый код? Зачем? В рабочем коде я бы предпочел просто AnimalModel со своими свойствами. В худшем случае, AmphibianModel и FowlModel, если их свойства и обработка настолько различны.

Это то, что я ожидал бы увидеть в текущем "ОО" языке:

public class Animal
{
    public int AnimalID { get; set; }
    public int LegCount { get; set; }
    public string Name { get; set; }
    public string WhatISay { get; set; }
}

public class AnimalService : IManageAnimals
{
    private IPersistAnimals _animalRepo;
    public AnimalService(IPersistAnimals animalRepo) { _animalRepo = animalRepo; }

    public List<Animal> GetAnimals() => _animalRepo.GetAnimals();

    public string WhatDoISay(Animal animal)
    {
        if (!string.IsNullOrWhiteSpace(animal.WhatISay))
            return animal.WhatISay;

        return _animalRepo.GetAnimalNoise(animal.AnimalID);
    }
}

Основной поток

Как вы переходите от классов в ОО к функциональному программированию? Как уже говорили другие; Вы можете, но на самом деле это не так. Суть вышесказанного заключается в том, чтобы продемонстрировать, что вам даже не следует использовать классы (в традиционном понимании мира) при выполнении Java и C #. Когда вы начнете писать код в сервис-ориентированной архитектуре (DDD, Многоуровневая, Многоуровневая, Шестиугольная и т. Д.), Вы станете на один шаг ближе к Функциональным, поскольку вы отделяете свои Данные (Доменные модели) от Логических функций (Сервисов).

ОО Язык на шаг ближе к ФП

Вы можете даже пойти немного дальше и разделить свои SOA-сервисы на два типа.

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

Необязательный тип класса 2 : Pure Business Logic Services. Это статические классы, которые имеют «чистую» функциональность. В ПП «Чистый» означает отсутствие побочных эффектов. Он нигде явно не устанавливает состояние или постоянство. (См. Примечание 5)

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

Заметки

Примечание 1 : Оригинальный объектно-ориентированный дизайн «Rich» или «Active-Record» все еще существует. Существует много унаследованного кода, подобного тому, когда люди «делали это правильно» десять или более лет назад. В прошлый раз я видел, что такой код (сделано правильно) был из видеоигры Codebase на C ++, где они точно контролировали память и имели очень ограниченное пространство. Нельзя сказать, что FP и сервис-ориентированные архитектуры - звери и не должны учитывать аппаратное обеспечение. Но они ставят возможность постоянно меняться, поддерживаться, иметь переменные размеры данных и другие аспекты в качестве приоритета. В компьютерных играх и искусственном интеллекте вы очень точно управляете сигналами и данными.

Примечание 2 : Модели предметной области не имеют полиморфного поведения и не имеют внешних зависимостей. Они «изолированы». Это не значит, что они должны быть на 100% анемичными. У них может быть много логики, связанной с их конструкцией и изменчивым изменением свойств, если это применимо. См. DDD «Объекты ценности» и сущности Эрика Эванса и Марка Симанна.

Примечание 3 : Линк и Лямбда очень распространены. Но когда пользователь создает новую функцию, он редко использует Func или Callable в качестве параметров, тогда как в FP было бы странно видеть приложение без функций, следующих этому шаблону.

Примечание 4 : Не путать полиморфизм с наследованием. CatModel может наследовать AnimalBase, чтобы определить, какими свойствами обычно обладает Animal. Но, как я показываю, такие модели - это запах кода . Если вы видите этот шаблон, вы можете подумать о том, чтобы разбить его и превратить в данные.

Примечание 5 : Чистые функции могут (и делают) принимать функции в качестве параметров. Входящая функция может быть нечистой, но может быть чистой. Для целей тестирования это всегда будет чисто. Но при производстве, хотя он рассматривается как чистый, он может содержать побочные эффекты. Это не меняет того факта, что чистая функция чистая. Хотя функция параметра может быть нечистой. Не путать! : D


-2

Вы могли бы сделать что-то вроде этого .. PHP

    function say($whostosay)
    {
        if($whostosay == 'cat')
        {
             return 'purr';
        }elseif($whostosay == 'dog'){
             return 'bark';
        }else{
             //do something with errors....
        }
     }

     function speak($whostosay)
     {
          return $whostosay .'\'s '.say($whostosay);
     }
     echo speak('cat');
     >>>cat's purr
     echo speak('dog');
     >>>dogs's bark

1
Я не дал никаких отрицательных голосов. Но я думаю, что это потому, что этот подход не является ни функциональным, ни объектно-ориентированным.
Маной Р

1
Но переданная концепция близка к сопоставлению с образцом, используемому в функциональном программировании, то есть $whostosayстановится типом объекта, который определяет, что будет выполнено. Выше можно изменить, чтобы дополнительно принять другой параметр, $whattosayчтобы тип, который его поддерживает (например 'human'), мог использовать его.
сёкит
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.