Какие функциональные возможности стоят немного путаницы ООП для выгоды, которые они приносят?

13

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

Возможные аспекты ФП:

  • неизменность
  • Частичное применение и карри
  • Функции первого класса (указатели на функции / функциональные объекты / шаблон стратегии)
  • Ленивая оценка (и монады)
  • Чистые функции (без побочных эффектов)
  • Выражения (в сравнении с утверждениями - каждая строка кода создает значение вместо или в дополнение к побочным эффектам)
  • Рекурсия
  • Сопоставление с образцом

Является ли это свободным для всех, где мы можем делать все, что поддерживает язык программирования, до тех пор, пока язык не поддерживает его? Или есть лучшее руководство?

Трезубец Д'Гао
источник
6
У меня был похожий опыт. Примерно через 2 месяца боли я начал находить довольно хороший баланс «вещей, которые отображаются на объекты» и «вещей, которые отображаются на функции». Это помогает сделать серьезный взлом на языке, который поддерживает оба. В конце концов, мои навыки FP и ООП значительно улучшились
Даниэль Гратцер
3
FWIW, Linq является функциональным и ленивым, и вы можете моделировать функциональное программирование в C #, используя статические методы и избегая сохранения состояния.
Роберт Харви
1
На данный момент, вы должны прочитать SICP . Это бесплатно и хорошо написано. Это предлагает хорошее сравнение между двумя парадигмами.
Саймон Бергот
4
FP и OOP одновременно являются в некотором смысле ортогональными и в некотором смысле двойственными. ООП - это абстракция данных, ФП - об (отсутствии) побочных эффектов. Наличие у вас побочных эффектов ортогонально тому, как вы абстрагируете свои данные. Например, лямбда-исчисление является как функциональным, так и объектно-ориентированным. Да, FP обычно использует абстрактные типы данных, а не объекты, но вы также можете использовать объекты, не будучи менее FP. OTOH, существует также глубокая связь: функция изоморфна объекту только с одним методом (именно так они «подделаны» в Java и реализованы в Java8,…
Йорг Миттаг,
3
Я думаю, что самый сильный аспект вашего вопроса связан с удобочитаемостью. "Какой стиль функционального программирования подходит для работы в объектно-ориентированном магазине?" Или то, что функциональные функции стоят немного путаницы ООП для выгоды, которые они приносят.
ГленПетерсон

Ответы:

13

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

Мы все обучены справляться и чувствуем себя комфортно с определенным уровнем сложности. Мне нравится называть это лимитом вознаграждения человека (от Watership Down, как высоко вы можете считать). Это отличная вещь, чтобы расширить свой кругозор, способность рассмотреть больше вариантов и иметь больше инструментов для подхода и решения проблем. Но это изменение, и оно вырывает вас из вашей зоны комфорта.

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

Удачи!

ChuckCottrill
источник
3
Я хотел бы добавить, что можно лучше понять некоторые традиционные концепции ООП при работе с функциональными языками, такими как Haskell или Clojure. Лично я понял, что полиморфизм действительно важная концепция (интерфейсы в Java или классы типов в Haskell), в то время как наследование (то, что я считал определяющим понятием) является своего рода странной абстракцией.
wirrbel
6

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

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

Моя любимая комбинация может выглядеть довольно тривиально, однако я считаю, что она очень сильно влияет на производительность. Эта комбинация является частичным применением и функциями каррирования и первого класса, которые я бы никогда не написал снова для цикла for : вместо этого передайте тело цикла в функцию итерации или отображения. Меня недавно наняли на работу в C ++, и я забавно заметил, что я полностью утратил привычку писать циклы for!

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

user40989
источник
2
Я полностью согласен, но я получил отпор от людей, которые по всей отрасли склонны соглашаться: они знают, что они используют шаблон посетителей, они видели и использовали его много раз, поэтому код в нем - это то, что они понимают и знакомы, другой подход, хотя до смешного проще и легче, чужой и, следовательно, более сложный для них. Это прискорбный факт того, что в отрасли более 15 лет ООП сталкивается с каждой мыслью программистов, что более 100 строк кода для них легче понять, чем 10, просто потому, что они запомнили эти 100+ строк после повторения их в течение десятилетия.
Джимми Хоффа
1
-1 - Более краткий код не означает, что вы пишете «меньше» кода. Вы пишете тот же код, используя меньше символов. Во всяком случае, вы делаете больше ошибок, потому что код (часто) труднее читать.
Теластин
8
@Telastyn: Terse - это не то же самое, что нечитаемое. Кроме того, огромные массы раздутого шаблона имеют свой собственный способ быть нечитаемым.
Майкл Шоу
1
@Telastyn Я думаю, что вы только что затронули реальную суть здесь, да, краткость может быть плохой и нечитаемой, раздутая может быть плохой и нечитаемой, но ключ не в переменной длине, а в загадочно написанном коде. Главное, как вы упомянули выше количество операций, я не согласен с тем, что количество операций не связано с удобством обслуживания, я думаю, что выполнение меньшего количества операций (с четко написанным кодом) приносит пользу удобочитаемости и удобству обслуживания. Очевидно, что делать то же самое с однобуквенными функциями и именами переменных не поможет, хороший FP требует значительно меньшего количества операций, все еще четко написанных
Джимми Хоффа
2
@ user949300: если вам нужен совершенно другой пример, как насчет этого примера Java 8 ?: list.forEach(System.out::println);С точки зрения FP, printlnфункция принимает два аргумента, цель PrintStreamи значение, Objectно метод Collections forEachожидает функцию с одним аргументом только который может быть применен к каждому элементу. Таким образом, первый аргумент привязан к экземпляру, найденному при System.outпереходе к новой функции с одним аргументом. Это проще, чемBiConsumer<…> c=PrintStream::println; PrintStream a1=System.out; list.forEach(a2 -> c.accept(a1, a2));
Хольгер
5

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

FP и OOP не являются взаимоисключающими. Посмотри на Скалу. Некоторые думают, что это худшее, потому что это нечистое FP, но некоторые думают, что это лучшее по той же причине.

Один за другим, вот некоторые аспекты, которые прекрасно работают с ООП:

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

  • Неизменность: String, возможно, является наиболее часто используемым объектом Java и является неизменным. Я рассказываю об неизменных объектах Java и неизменных коллекциях Java в своем блоге. Часть этого может быть применима к вам.

  • Функции первого класса (указатели на функции / функциональные объекты / шаблон стратегии). Начиная с версии 1.1 в Java существовала мутантная версия этого мутанта с большинством классов API (а их сотни), которые реализуют интерфейс Listener. Runnable, вероятно, наиболее часто используемый функциональный объект. Функции первого класса - это больше работы для написания кода на языке, который не поддерживает их изначально, но иногда стоит дополнительных усилий, когда они упрощают другие аспекты вашего кода.

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

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

  • Ленивая оценка (и монады) - в основном ограничивается отложенной инициализацией в ООП. Без языковых функций для его поддержки вы можете найти некоторые полезные API-интерфейсы, но написать свой собственный сложно. Вместо этого максимально увеличьте использование потоков - примеры интерфейсов см. В интерфейсах Writer и Reader.

  • Частичное применение и каррирование - не практично без функций первого класса.

  • Pattern Matching - вообще не рекомендуется в ООП.

Таким образом, я не думаю, что работа должна быть бесплатной для всех, где вы можете делать все, что поддерживает язык программирования, до тех пор, пока язык не поддерживает его. Я думаю, что читабельность ваших коллег должна быть вашим лакмусовым тестом для кода, созданного по найму. Где это вас раздражает больше всего, я бы хотел начать обучение на работе, чтобы расширить кругозор ваших коллег.

GlenPeterson
источник
С тех пор, как я изучал FP, я привык проектировать вещи так, чтобы они имели плавные интерфейсы, что приводит к тому, что сродни выражениям, функция, которая имеет одно утверждение, которое делает кучу вещей. Это самое близкое, что вы действительно получите, но это подход, который естественным образом вытекает из чистоты, когда вы обнаружите, что у вас больше нет пустых методов, использование статических методов расширения в C # помогает в этом. Таким образом, ваша точка зрения - это единственная точка зрения, с которой я не согласен, все остальное уместно в моем собственном опыте изучения FP и работы на .NET на дневную работу
Джимми Хоффа
Что действительно беспокоит меня в C # сейчас, так это то, что я не могу использовать делегаты вместо интерфейсов с одним методом по двум простым причинам: 1. Вы не можете создавать рекурсивные лямбсы без взлома (присвоение сначала нуля, а затем лямбда-секунды) или Y- комбинатор (который так же безобразен, как ад в C #). 2. нет псевдонимов типов, которые вы можете использовать в области проекта, поэтому подписи ваших делегатов довольно быстро становятся неуправляемыми. Так что по этим двум глупым причинам я больше не могу наслаждаться C #, потому что единственное, что я могу заставить его работать, это использовать интерфейсы с одним методом, что является просто ненужной дополнительной работой.
Трайдент
@bonomo Java 8 имеет общий java.util.function.BiConsumer, который может быть полезен в C #: public interface BiConsumer<T, U> { public void accept(T t, U u); }в java.util.function есть и другие полезные функциональные интерфейсы.
ГленПетерсон
@bonomo Эй, я понял, это боль Хаскелла. Всякий раз, когда вы читаете, кто-то говорит «Изучение ФП сделало меня лучше в ООП», это означает, что они изучали Ruby или что-то не такое чистое и декларативное, как Haskell. Haskell дает понять, что ООП - бесполезно низкий уровень. Самая большая боль, с которой вы сталкиваетесь, заключается в том, что вывод типов на основе ограничений неразрешим, когда вы не в системе типов HM, поэтому вывод типов на основе cosntraint совершенно не выполняется: blogs.msdn.com/b/ericlippert/archive / 2012/03/09 /…
Джимми Хоффа
1
Most OOP languages don't have the stack space for it В самом деле? Все, что вам нужно, это 30 уровней рекурсии для управления миллиардами узлов в сбалансированном двоичном дереве. Я уверен, что мое пространство стека подходит для гораздо большего количества уровней, чем этот.
Роберт Харви
3

Помимо функционального программирования и объектно-ориентированного программирования, существует также декларативное программирование (SQL, XQuery). Изучение каждого стиля поможет вам получить новое понимание, и вы научитесь выбирать правильный инструмент для работы.

Но да, это может быть очень сложно писать код на языке, и знать, что если бы вы использовали что-то другое, вы могли бы быть более продуктивным для конкретной проблемной области. Тем не менее, даже если вы используете такой язык, как Java, можно применять концепции из FP к вашему Java-коду, хотя и окольными путями. Например, инфраструктура Guava делает это.

sgwizdak
источник
2

Как программист, я думаю, что вы никогда не должны прекращать учиться. Тем не менее, очень интересно, что изучение FP портит ваши навыки ООП. Я склонен думать об обучении ООП как об обучении катанию на велосипеде; Вы никогда не забудете, как это сделать.

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

По мере того, как вы приобретаете больше опыта, основные концепции программирования будет гораздо труднее потерять. Поэтому я советую вам спокойно относиться к FP, пока концепции ООП полностью не закрепятся в вашем уме. FP - это определенный сдвиг парадигмы. Удачи!

Бобби Гэммилл
источник
4
Обучение ООП похоже на обучение ползанию. Но как только вы встанете на ноги, вы будете ползти только тогда, когда вы слишком пьяны. Конечно, вы не можете забыть, как это сделать, но обычно вы этого не хотите. И ходить с гусеницами будет мучительно, если ты знаешь, что можешь бежать.
SK-logic
@ SK-logic, мне нравится твоя метафора
Trident D'Gao
@ SK-Logic: Каково обучение императивному программированию? Таскаешь себя на животе?
Роберт Харви
@RobertHarvey пытается спрятаться под землёй с ржавой ложкой и колодой перфокарт.
Джимми Хоффа
0

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

Если вы используете C ++ 11, существует много таких функциональных функций программирования, встроенных в языковую / стандартную библиотеку, которые хорошо сочетаются с ООП. Конечно, я не уверен, насколько хорошо TMP будет принят вашим начальником или коллегами, но дело в том, что вы можете получить многие из этих функций в той или иной форме на нефункциональных языках / языках ООП, таких как C ++.

Использование шаблонов с рекурсией во время компиляции зависит от ваших первых 3 баллов,

  • неизменность
  • Рекурсия
  • Сопоставление с образцом

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

Что касается других пунктов, использование std::bindи std::functionдает вам частичное применение функции, а указатели функций встроены в язык. Вызываемые объекты являются функциональными объектами (а также частичным применением функций). Обратите внимание, что под вызываемыми объектами я имею в виду те, которые определяют их operator ().

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

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

#include <iostream>
#include <functional>

//holds a compile-time index sequence
template<std::size_t ... >
struct index_seq
{};

//builds the index_seq<...> struct with the indices (boils down to compile-time indexing)
template<std::size_t N, std::size_t ... Seq>
struct gen_indices
  : gen_indices<N-1, N-1, Seq ... >
{};

template<std::size_t ... Seq>
struct gen_indices<0, Seq ... >
{
    typedef index_seq<Seq ... > type;
};


template <typename RType>
struct bind_to_fcn
{
    template <class Fcn, class ... Args>
    std::function<RType()> fcn_bind(Fcn fcn, std::tuple<Args...> params)
    {
        return bindFunc(typename gen_indices<sizeof...(Args)>::type(), fcn, params);
    }

    template<std::size_t ... Seq, class Fcn, class ... Args>
    std::function<RType()> bindFunc(index_seq<Seq...>, Fcn fcn, std::tuple<Args...> params)
    {
        return std::bind(fcn, std::get<Seq>(params) ...);
    }
};

//some arbitrary testing function to use
double foo(int x, float y, double z)
{
    return x + y + z;
}

int main(void)
{
    //some tuple of parameters to use in the function call
    std::tuple<int, float, double> t = std::make_tuple(1, 2.04, 0.1);                                                                                                                                                                                                      
    typedef double(*SumFcn)(int,float,double);

    bind_to_fcn<double> binder;
    auto other_fcn_obj = binder.fcn_bind<SumFcn>(foo, t);
    std::cout << other_fcn_obj() << std::endl;
}
alrikai
источник