Jakie funkcje są warte trochę pomieszania OOP z korzyściami, które przynoszą?

13

Po nauczeniu się programowania funkcjonalnego w języku Haskell i F #, paradygmat OOP wydaje się działać wstecz w stosunku do klas, interfejsów, obiektów. Jakie aspekty PR mogę wnieść do pracy, które moi współpracownicy mogą zrozumieć? Czy są jakieś style FP warte rozmowy z moim szefem na temat przekwalifikowania mojego zespołu, abyśmy mogli z nich korzystać?

Możliwe aspekty PR:

  • Niezmienność
  • Częściowe zastosowanie i curry
  • Funkcje pierwszej klasy (wskaźniki funkcji / obiekty funkcjonalne / wzorzec strategii)
  • Leniwa ocena (i monady)
  • Pure Functions (bez skutków ubocznych)
  • Wyrażenia (vs. Instrukcje - każdy wiersz kodu generuje wartość zamiast lub oprócz powodowania efektów ubocznych)
  • Rekurencja
  • Dopasowywanie wzorów

Czy jest to darmowe rozwiązanie, w którym możemy robić wszystko, co obsługuje język programowania, do poziomu, w jakim język go obsługuje? Czy jest lepsza wytyczna?

Trident D'Gao
źródło
6
Miałem podobne doświadczenie. Po około 2 miesiącach bólu zacząłem znaleźć całkiem niezłą równowagę między „rzeczami, które mapują obiekty” i „rzeczami, które mapują funkcje”. Pomaga dokonać poważnego włamania w języku, który obsługuje oba te elementy. Na koniec znacznie poprawiły się moje umiejętności FP i OOP
Daniel Gratzer
3
FWIW, Linq jest zarówno funkcjonalny, jak i leniwy, i można symulować programowanie funkcjonalne w języku C # przy użyciu metod statycznych i unikając utrzymywania stanu.
Robert Harvey
1
W tym momencie powinieneś przeczytać sicp . Jest darmowy i dobrze napisany. Oferuje ładne porównanie dwóch paradygmatów.
Simon Bergot,
4
FP i OOP są jednocześnie w pewnym sensie ortogonalne, aw pewnym sensie dualne. OOP dotyczy pozyskiwania danych, FP dotyczy (braku) skutków ubocznych. To, czy masz skutki uboczne, jest prostopadłe do tego, jak pobierasz dane. Rachunek Lambda jest zarówno funkcjonalny, jak i obiektowy. Tak, FP zwykle używa Abstrakcyjnych Typów Danych, a nie obiektów, ale równie dobrze można użyć obiektów zamiast być mniej FP. OTOH, istnieje również głęboki związek: funkcja jest izomorficzna w stosunku do obiektu za pomocą tylko jednej metody (w ten sposób są one „sfałszowane” w Javie i zaimplementowane w Java8,…
Jörg W Mittag
3
Myślę, że najsilniejszy aspekt twojego pytania dotyczy czytelności. „W jakim stopniu styl programowania funkcjonalnego jest odpowiedni do pracy w sklepie zorientowanym obiektowo?” Lub jakie cechy funkcjonalne są warte trochę zamieszania związanego z OOP, jeśli chodzi o korzyści, jakie przynoszą.
GlenPeterson

Odpowiedzi:

13

Programowanie funkcjonalne jest innym paradygmatem niż programowanie obiektowe (inny sposób myślenia i inny sposób myślenia o programach). Zacząłeś zdawać sobie sprawę, że jest więcej niż jeden sposób (obiektowy) myślenia o problemach i ich rozwiązaniach. Są inne (przychodzi na myśl programowanie proceduralne i ogólne). To, jak zareagujesz na tę nową wiedzę, niezależnie od tego, czy zaakceptujesz i zintegrujesz te nowe narzędzia i podejścia z zestawem umiejętności, określi, czy rozwijasz się i stajesz się bardziej kompletnym, wykwalifikowanym programistą.

Wszyscy jesteśmy przeszkoleni do obsługi i czujemy się komfortowo z pewnym poziomem złożoności. Lubię nazywać to osoby hrair limitu (od Watership Down, jak wysoko można liczyć). Wspaniale jest rozwinąć swój umysł, zdolność do rozważenia większej liczby opcji i posiadania większej liczby narzędzi do podejścia i rozwiązywania problemów. Ale to zmiana i wyciąga cię ze strefy komfortu.

Jednym z problemów, które możesz napotkać, jest to, że będziesz mniej zadowolony z podążania za tłumem „wszystko jest przedmiotem”. Być może będziesz musiał wykazać się cierpliwością podczas pracy z ludźmi, którzy mogą nie rozumieć (lub chcą zrozumieć), dlaczego funkcjonalne podejście do tworzenia oprogramowania działa dobrze w przypadku niektórych problemów. Podobnie jak ogólne podejście programistyczne działa dobrze w przypadku niektórych problemów.

Powodzenia!

ChuckCottrill
źródło
3
Chciałbym dodać, że można lepiej zrozumieć niektóre tradycyjne koncepcje OOP podczas pracy z funkcjonalnymi językami, takimi jak Haskell lub Clojure. Osobiście zdałem sobie sprawę, że polimorfizm jest tak naprawdę ważną koncepcją (interfejsy w Javie lub typy w Haskell), podczas gdy dziedziczenie (to, co uważałem za definicję) jest rodzajem dziwnej abstrakcji.
wirrbel
6

Programowanie funkcjonalne zapewnia bardzo praktyczną, przyziemną produktywność w codziennym pisaniu kodu: niektóre funkcje sprzyjają zwięzłości, co jest świetne, ponieważ im mniej piszesz kodu, tym mniej błędów popełniasz i wymaga mniej konserwacji.

Jako matematyk uważam, że fantazyjne funkcje są bardzo atrakcyjne, ale zwykle są przydatne podczas projektowania aplikacji: struktury te mogą kodować w strukturze programu wiele niezmienników programu, bez reprezentowania tych niezmienników przez zmienne.

Moja ulubiona kombinacja może wyglądać dość trywialnie, jednak uważam, że ma bardzo duży wpływ na wydajność. Ta kombinacja to Częściowa Aplikacja i Funkcje Currying i Pierwszej Klasy, którą chciałbym ponownie opatrzyć etykietą, nigdy więcej nie napisać pętli for : zamiast tego przekaż ciało pętli do funkcji iteracji lub mapowania. Niedawno zostałem zatrudniony do pracy w C ++ i zabawnie zauważyłem, że całkowicie straciłem nawyk pisania dla pętli!

Połączenie Rekurencji i Dopasowywania Wzorów eliminuje potrzebę tego wzorca projektowego Odwiedzającego . Wystarczy porównać kod potrzebny do zaprogramowania ewaluatora wyrażeń boolowskich: w dowolnym funkcjonalnym języku programowania powinno to być około 15 wierszy kodu, w OOP właściwym rozwiązaniem jest użycie wzorca projektu Visitor , który zamienia ten przykład zabawki w obszerny esej. Korzyści są oczywiste i nie jestem świadomy żadnych niedogodności.

użytkownik40989
źródło
2
Zgadzam się całkowicie, ale odepchnąłem mnie od ludzi, którzy w całej branży zgadzają się: wiedzą, że mają wzór odwiedzin, widzieli go i używali wiele razy, więc kod w nim jest czymś, co rozumieją i znają, inne podejście, choć absurdalnie prostsze i łatwiejsze, jest obce, a zatem trudniejsze dla nich. Jest to niefortunny fakt, że w branży ponad 15 lat OOP uderzyło w głowę każdego programisty, że ponad 100 linii kodu jest dla nich łatwiejsze do zrozumienia niż 10 po prostu dlatego, że zapamiętali ponad 100 linii po powtórzeniu ich przez dekadę
Jimmy Hoffa
1
-1 - Więcej zwięzłych kodów nie oznacza, że ​​piszesz kod „mniej”. Piszesz ten sam kod używając mniej znaków. Jeśli już, popełnisz więcej błędów, ponieważ kod jest (często) trudniejszy do odczytania.
Telastyn
8
@Telastyn: Terse to nie to samo, co nieczytelne. Również ogromne masy nadęty bojler mają swój własny sposób na nieczytelność.
Michael Shaw
1
@Telastyn Myślę, że właśnie dotknąłeś prawdziwego sedna tutaj, tak zwięzły może być zły i nieczytelny, nadęty może być zły i nieczytelny, ale kluczem nie jest zmienna długość i zagadkowo napisany kod. Kluczem jest, jak wspomina powyżej liczby operacji, nie zgadzam się, że liczba operacji nie koreluje do konserwacji, myślę, że robi mniej rzeczy (z kodem wyraźnie napisane) robi czytelność korzyści oraz konserwacji. Oczywiście robienie tej samej liczby rzeczy za pomocą funkcji pojedynczej litery i nazw zmiennych nie pomoże, dobry FP potrzebuje znacznie mniej operacji, wciąż wyraźnie napisanych
Jimmy Hoffa
2
@ user949300: jeśli chcesz całkowicie inny przykład, jak o tym Java 8 przykład ?: list.forEach(System.out::println);z punktu widzenia PR, printlnjest funkcją biorąc dwa argumenty, cel PrintStreami wartość Objectale Collection„s forEachmetody oczekuje funkcję z jednym argumentem tylko które można zastosować do każdego elementu. Tak więc pierwszy argument jest powiązany z instancją znajdującą się w System.outnowej funkcji z jednym argumentem. To prostsze niżBiConsumer<…> c=PrintStream::println; PrintStream a1=System.out; list.forEach(a2 -> c.accept(a1, a2));
Holger
5

Być może będziesz musiał ograniczyć to, z jakiej części swojej wiedzy korzystasz w pracy, tak jak Superman udaje, że jest Clarkiem Kentem, aby cieszyć się przywilejami normalnego życia. Ale wiedza więcej nigdy cię nie skrzywdzi. To powiedziawszy, niektóre aspekty programowania funkcjonalnego są odpowiednie dla sklepu zorientowanego obiektowo, a inne aspekty mogą być warte rozmowy z szefem, abyś mógł podnieść średni poziom wiedzy swojego sklepu i dzięki temu napisać lepszy kod.

FP i OOP nie wykluczają się wzajemnie. Spójrz na Scalę. Niektórzy uważają, że jest najgorszy, ponieważ jest to nieczyste FP, ale niektórzy uważają, że jest najlepszy z tego samego powodu.

Oto kilka aspektów, które świetnie współpracują z OOP:

  • Funkcje Pure (bez skutków ubocznych) - obsługuje każdy język programowania, który znam. Sprawiają, że Twój kod jest o wiele łatwiejszy do rozumowania i powinien być używany, gdy tylko jest to praktyczne. Nie musisz nazywać tego FP. Po prostu nazwij to dobrą praktyką kodowania.

  • Niezmienność: Ciąg jest prawdopodobnie najczęściej używanym obiektem Java i jest niezmienny. I okładka Niezmienne obiekty Java i niezmienne Java Collections na moim blogu. Niektóre z nich mogą dotyczyć Ciebie.

  • Funkcje pierwszej klasy (wskaźniki funkcji / obiekty funkcjonalne / wzorzec strategii) - Java ma spłaszczoną, zmutowaną wersję tego od wersji 1.1 z większością klas API (i są ich setki), które implementują interfejs Listener. Runnable jest prawdopodobnie najczęściej używanym obiektem funkcjonalnym. Funkcje pierwszej klasy wymagają więcej pracy przy kodowaniu w języku, który nie obsługuje ich natywnie, ale czasem jest warty dodatkowego wysiłku, gdy upraszczają inne aspekty twojego kodu.

  • Rekurencja jest przydatna do przetwarzania drzew. W sklepie OOP jest to prawdopodobnie podstawowe właściwe zastosowanie rekurencji. Korzystanie z rekurencji dla zabawy w OOP powinno być raczej obrzydzone, jeśli z innego powodu niż w większości języków OOP domyślnie nie ma miejsca na stos, aby uczynić to dobrym pomysłem.

  • Wyrażenia (vs. Instrukcje - każdy wiersz kodu wytwarza wartość zamiast lub oprócz powodowania skutków ubocznych) - Jedynym operatorem oceniającym w C, C ++ i Javie jest operator trójskładnikowy . Omawiam odpowiednie użycie na moim blogu. Może się okazać, że piszesz kilka prostych funkcji, które nadają się do wielokrotnego użytku i oceniają.

  • Leniwa ocena (i monady) - głównie ograniczona do leniwej inicjalizacji w OOP. Bez obsługujących je funkcji językowych możesz znaleźć przydatne interfejsy API, ale napisanie własnego jest trudne. Zamiast tego zmaksymalizuj wykorzystanie strumieni - zobacz przykłady interfejsów Writer i Reader.

  • Częściowe zastosowanie i curry - niepraktyczne bez funkcji pierwszej klasy.

  • Dopasowywanie wzorców - ogólnie odradzane w OOP.

Podsumowując, nie uważam, że praca powinna być czymś darmowym, w którym możesz robić wszystko, co obsługuje język programowania, do tego stopnia, że ​​język go obsługuje. Myślę, że czytelność twoich współpracowników powinna być sprawdzianem lakmusowym kodu przygotowanego na wynajem. Tam, gdzie najbardziej cię to denerwuje, chciałbym rozpocząć naukę w pracy, aby poszerzyć horyzonty swoich współpracowników.

GlenPeterson
źródło
Od czasu nauki FP przyzwyczaiłem się projektować rzeczy tak, aby miały płynne interfejsy, co skutkuje podobnym do wyrażeń, funkcją, która ma jedno zdanie, które robi wiele rzeczy. Jest to najbliższe, jakie naprawdę dostaniesz, ale jest to podejście, które wypływa naturalnie z czystości, gdy okazuje się, że nie masz już żadnych metod pustki, przy użyciu metod rozszerzania statycznego w języku C # bardzo to pomaga. W ten sposób twój punkt wyrażenia jest jedynym punktem, z którym się nie zgadzam, wszystko inne jest na czasie z moimi własnymi doświadczeniami, ucząc się FP i pracując w dzień .NET
Jimmy Hoffa
W C # najbardziej przeszkadza mi teraz to, że nie mogę używać delegatów zamiast interfejsów jednoprocesowych z dwóch prostych powodów: 1. nie możesz tworzyć rekurencyjnych lambas bez hacka (najpierw przypisanie do null, a potem do lambda na sekundę) lub Y- kombinator (który jest tak brzydki jak diabli w C #). 2. nie ma aliasów typów, których można by użyć w zakresie projektu, więc podpisy delegatów dość szybko stają się niemożliwe do zarządzania. Więc właśnie z tych dwóch głupich powodów nie mogę już cieszyć się C #, ponieważ jedyne, co mogę sprawić, żeby działało, to używając interfejsów jednoprocesowych, co jest po prostu niepotrzebną dodatkową pracą.
Trident D'Gao
@bonomo Java 8 ma ogólny java.util.function.BiConsumer, który może być przydatny w C #: public interface BiConsumer<T, U> { public void accept(T t, U u); }Istnieją inne przydatne interfejsy funkcjonalne w java.util.function.
GlenPeterson
@bonomo Hej, rozumiem, to ból Haskella. Ilekroć czytasz, gdy ktoś mówi: „Uczenie się FP uczyniło mnie lepszym w OOP” oznacza, że ​​nauczył się Ruby lub czegoś, co nie jest czyste i deklaratywne, jak Haskell. Haskell wyjaśnia, że ​​OOP jest bezużytecznie niskim poziomem. Największy problem, na jaki napotykasz, polega na tym, że wnioskowanie o typach opartych na ograniczeniach jest nierozstrzygalne, gdy nie jesteś w systemie typów HM, więc wnioskowanie o typach opartych na ograniczeniach jest całkowicie niemożliwe: blogs.msdn.com/b/ericlippert/archive / 2012/03/09 /…
Jimmy Hoffa
1
Most OOP languages don't have the stack space for it Naprawdę? Wszystko czego potrzebujesz to 30 poziomów rekurencji do zarządzania miliardami węzłów w zrównoważonym drzewie binarnym. Jestem prawie pewien, że moje miejsce na stosie jest odpowiednie na wiele więcej poziomów niż to.
Robert Harvey
3

Oprócz programowania funkcjonalnego i programowania obiektowego istnieje również programowanie deklaratywne (SQL, XQuery). Poznanie każdego stylu pomaga uzyskać nowe informacje i nauczysz się wybierać odpowiednie narzędzie do pracy.

Ale tak, pisanie kodu w języku może być bardzo frustrujące i wiedzieć, że jeśli używasz czegoś innego, możesz być znacznie bardziej produktywny w przypadku określonej problematycznej domeny. Jednak nawet jeśli używasz języka takiego jak Java, możliwe jest zastosowanie pojęć z FP do kodu Java, aczkolwiek na różne sposoby. Na przykład część Guava robi to częściowo.

sgwizdak
źródło
2

Jako programista uważam, że nigdy nie powinieneś przestać się uczyć. To powiedziawszy, to bardzo interesujące, że uczenie się FP osłabia twoje umiejętności OOP. Uczę się OOP jako nauki jazdy na rowerze; nigdy nie zapomnisz, jak to zrobić.

Kiedy poznałem tajniki FP, zacząłem myśleć bardziej matematycznie i zyskałem lepszą perspektywę środków, w których piszę oprogramowanie. To moje osobiste doświadczenie.

W miarę zdobywania doświadczenia podstawowe koncepcje programowania będą znacznie trudniejsze do stracenia. Sugeruję więc, abyś rozluźnił FP, dopóki koncepcje OOP nie zostaną całkowicie utrwalone w twoim umyśle. FP jest zdecydowaną zmianą paradygmatu. Powodzenia!

Bobby Gammill
źródło
4
Nauka OOP jest jak nauka czołgania się. Ale kiedy już będziesz na nogach, będziesz się czołgać tylko wtedy, gdy będziesz zbyt pijany. Oczywiście nie możesz zapomnieć, jak to zrobić, ale normalnie nie chcesz. Spacer z pełzaczami będzie bolesnym doświadczeniem, gdy będziesz wiedział, że możesz biegać.
SK-logic,
@ SK-logic, podoba mi się twoja metafora
Trident D'Gao
@ SK-Logic: Jak wygląda nauka programowania imperatywnego? Ciągniesz się za brzuch?
Robert Harvey
@RobertHarvey próbuje zakopać pod ziemią zardzewiałą łyżką i pokładem kart perforowanych.
Jimmy Hoffa
0

Jest już wiele dobrych odpowiedzi, więc moje odniesie się do części twojego pytania; mianowicie, zastanawiam się nad założeniem twojego pytania, ponieważ OOP i funkcje nie wykluczają się wzajemnie.

Jeśli używasz C ++ 11, istnieje wiele tego rodzaju funkcji programowania funkcjonalnego wbudowanych w bibliotekę językową / standardową, które dobrze (ładnie) współdziałają z OOP. Oczywiście nie jestem pewien, jak dobrze TMP będzie odbierany przez twojego szefa lub współpracowników, ale chodzi o to, że możesz uzyskać wiele z tych funkcji w takiej czy innej formie w niefunkcjonalnych / OOP językach, takich jak C ++.

Używanie szablonów z rekurencją czasu kompilacji zależy od pierwszych 3 punktów,

  • Niezmienność
  • Rekurencja
  • Dopasowywanie wzorów

W tym, że wartości szablonu są niezmienne (stałe czasu kompilacji), każda iteracja jest wykonywana przy użyciu rekurencji, a rozgałęzianie odbywa się przy użyciu (mniej więcej) dopasowywania wzorców, w postaci rozdzielczości przeciążenia.

Jeśli chodzi o inne punkty, użycie std::bindi std::functiondaje częściową aplikację funkcji, a wskaźniki funkcji są wbudowane w język. Obiekty wywoływalne są obiektami funkcjonalnymi (a także częściową aplikacją funkcji). Zauważ, że przez obiekty, które można wywoływać, mam na myśli te, które je definiują operator ().

Leniwa ocena i czyste funkcje byłyby nieco trudniejsze; dla funkcji czystych można użyć funkcji lambda, które przechwytują tylko wartości, ale nie jest to idealne.

Wreszcie, oto przykład użycia rekurencji w czasie kompilacji z aplikacją funkcji częściowej. Jest to nieco wymyślony przykład, ale pokazuje większość powyższych punktów. Rekurencyjnie powiąże wartości w danej krotce z daną funkcją i wygeneruje obiekt funkcji (na żądanie)

#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
źródło