Programowanie funkcyjne – wprowadzenie
Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi językami programowania, ponieważ zawierają sekwencje zadań do wykonania. Programista jawnie, krok po kroku definiuje jak wykonać zadanie. Programowanie funkcyjne działa inaczej. Zamiast sekwencyjnie wykonywać zadania, języki funkcyjne wyznaczają jedynie wartości poszczególnych wyrażeń.
Programy funkcyjne składają się jedynie z funkcji. Główny program jest funkcją, której podajemy argumenty, a w zamian otrzymujemy wyznaczoną wartość – wynik działania programu. Główna funkcja składa się tylko i wyłącznie z innych funkcji, które z kolei składają się z jeszcze innych funkcji. Każda operacja wykonywana podczas działania funkcji, a nie mająca związku z wartością zwracaną przez funkcję to efekt uboczny (np. operacje wejścia wyjścia, modyfikowanie zmiennych globalnych). Funkcje które nie posiadają efektów ubocznych nazywane są funkcjami czystymi (pure function).
Ideą programowania funkcyjnego jest pisanie programów w kategoriach „co ma być osiągnięte”, a nie – jak w programowaniu imperatywnym – poprzez specyfikowanie kolejnych kroków algorytmu do wykonania. Takie podejście jest możliwe, gdy w języku programowania możemy traktować fragmenty kodu (funkcje) jako pełnoprawne obiekty, które mogą być przekazywane innym funkcjom i zwracane z innych funkcji. W czystych językach funkcyjnych tak właśnie się dzieje, co więcej – funkcje nie zmieniają żadnych danych (stanów), ważne jest jedynie wyliczanie ich wyników na podstawie podanych argumentów (co ma duże znaczenie np. przy przetwarzaniu równoległym).
Domknięcia(closure) – obiekt wiążący funkcję ze środowiskiem, w którym ona działa.
Zadaniem środowiska jest przechowywanie danych, nie mających charakteru globalnego, wykorzystywanych przez funkcję.
Domknięcia są nazywane także wyrażeniami lambda (lambda expressions).
W Javie w wersji 8 wprowadzono (w ograniczonym zakresie) wsparcie dla programowania funkcyjnego. Obejmuje ono przede wszystkim lambda-wyrażenia oraz przetwarzanie strumieniowe.
O lambda-wyrażenia możemy myśleć jako o bezpośrednio podawanych fragmentach kodu, swoistych funkcjach bez nazwy, które są traktowane „jak prawdziwe obiekty”, czyli np. mogą być przekazywane metodom.
W elastycznie wykorzystywanej metodzie (dla różnych typów danych, różnych warunków, różnych operacji) dzięki lambda-wyrażeniom sformułowanie tego, co chcemy osiągnąć, jest bardzo proste i czytelne.
Dla większej uniwersalności metod (być może chcielibyśmy najpierw przetworzyć dane, a filtrować wyniki tego przetwarzania. a może filtrować, przetwarzać i znowu filtrować?) często zastępujące i znacznie poszerzające ich funkcjonalność przydatne jest przetwarzanie strumieniowe.
Interfejsy funkcyjne i lambda-wyrażenia
Jeżeli zestaw metod, deklarowanych w interfejsie, zawiera tylko jedną metodę abstrakcyjną (wyłączając dodane deklaracje abstrakcyjnych publicznych niefinalnych metod z klasy Object), to taki interfejs nazywa się funkcyjnym (SAM – Single Abstract Method). Przykłady interfejsów funkcyjnych:
…
Lambda-wyrażenia można stosować jako uproszczenie w użyciu anonimowych klas wewnętrznych, implementujących interfejsy funkcyjne. Podobnie jak w lokalnych klasach wewnętrznych lambda-wyrażenie ma dostęp:
- do wszystkich składowych klasy otaczającej, przy czym pola tej klasy mogą być przez lambda-wyrażenie modyfikowane;
- do zmiennych lokalnych, ale muszą one albo być deklarowane ze specyfikatorem final albo być efektywnie finalne (effective final).
Inaczej niż w anonimowych klasach wewnętrznych lambda-wyrażenie nie wprowadza nowego zasięgu widoczności i dostępności, co oznacza m.in., że
- zmienna this jest referencją do obiektu klasy, w której tworzone jest lambda-wyrażenie (w klasie anonimowej this oznacza obiekt tej klasy);
- słowo kluczowe super oznacza nadklasę obiektu (klasy), w której tworzone jest lambda-wyrażenie;
- jeśli lambda-wyrażenie występuje w bloku, niedopuszczalne jest przesłanianie zmiennych lokalnych z bloku otaczającego (w klasie lokalnej anonimowej mamy nowy zasięg widoczności, więc możemy używać tych samych identyfikatorów zmiennych, co w bloku otaczającym).
Referencje do metod i konstruktorów
Często definicja lambda-wyrażenia sprowadza się do wywołania metody istniejącej klasy lub utworzenia jej obiektu. Wtedy zamiast wywołania tej metody w lambda-wyrażeniu możemy zastosować referencję do metody lub konstruktora. Są cztery rodzaje takich referencji (symbolicznie przez x oznaczamy jeden parametr lub listę parametrów w nawiasach okrągłych):
● NazwaKlasy::nazwa_metody_statycznej
Klasa::metStat – odpowiada lambda-wyrażeniu
x -> Klasa.metStat(x)
● NazwaKlasy::nazwa_metody_niestatycznej
Klasa::met – odpowiada lambda-wyrażeniu
x -> x.met()
● zmienna_niestatyczna::nazwa_metody_niestatycznej
Klasa v;
v::met – odpowiada lambda-wyrażeniu
x -> v.met(x)
● NazwaKlasy::new
Klasa::new – odpowiada lambda-wyrażeniu
x -> new Klasa(x)
Naturalnie, liczba i typy parametrów oraz typ wyniku metody (konstruktora), do których używamy referencji, muszą odpowiadać deskryptorowi funkcji jakiegoś interfejsu funkcyjnego (który zostanie dobrany do realizacji lambda-wyrażenia).
przykład str.24-26 http://pracownik.kul.pl/files/10382/public/W13.pdf
Gotowe interfejsy funkcyjne
Najczęściej nie musimy tworzyć własnych interfejsów funkcyjnych.
W Javie są już interfejsy typu SAM (np. FileFilter czy Runnable). Dodatkowo w wersji 8 pojawił się pakiet java.util.function, w którym zdefiniowano wiele interfejsów funkcyjnych dla częstych przypadków użycia lambda-wyrażeń, np.
Predicate<T>, Function<S, T>, UnaryOperator<T>, Supplier<T>, Consumer<T>(), BiPredicate<U,V>, BiFunction<U,V,R>, BinaryOperator<T>, BiConsumer<U,V>.
Interfejsy z których już korzystaliśmy:
A) ActionListener – ma jedną metodę:
void actionPerformed(ActionEvent ae)
B)FileFilter – ma jedną metodę:
boolean accept(File f)
wykorzystywaną do selekcji obiektów plikowych np. w trakcie listowania zawartości katalogu metodą listFiles (tu jako argument podajemy właśnie implementację metody accept). Zatem dla uzyskania tablicy plików z danego katalogu dir, mających podane rozszerzenie ext i czas modyfikacji późniejszy od podanego time możemy napisać:
File[] files = new File(dir).listFiles(f -> f.isFile() && f.getName().endsWith(„.”+ext)&& f.lastModified() > time);
C) Comparator – ma jedną metodę:
int compare(T obiekt1, T obiekt2)
Przy porównywaniu i sortowaniu możemy więc używać wygodnych lambda-wyrażeń, np. do uporządkowania listy napisów wg ich długości:
List<String> lst = Arrays.asList(„ala”, „ma”, „kota”, „i”, „pieska”);
Collections.sort(lst, (s1, s2) -> s2.length() – s1.length());
System.out.println(lst);
Powyższy fragment wyprowadzi: [pieska, kota, ala, ma, i]
D)Runnable
Kod zliczający czas (np. w trakcie jakiejś interakcji użytkownika z programem) za pomocą lambda-wyrażenia, pasującego do deskryptora funkcji (public void run()) moglibyśmy napisać tak:
Future<?> ftask = Executors.newSingleThreadExecutor().submit( () -> { int sec = 0; while(true) { sec++; try { Thread.sleep(1000); } catch(InterruptedException exc) { return; } System.out.println(sec/60 + „:” + sec%60);}});
// …
JOptionPane.showMessageDialog(null, „Close dialog to stop”);
ftask.cancel(true);
E) Callable – ma jedną metodę
V call() throws Exception
Lambda-wyrażenia w gotowych metodach
… (str.30-36) … http://pracownik.kul.pl/files/10382/public/W13.pdf
INTERFEJSY FUNKCYJNE
@FunctionalInterface – jest opcjonalne
Każdy interfejs posiadający dokładnie jedną metodę abstrakcyjną jest interfejsem funkcyjnym, a zatem interfejsami funkcyjnymi są m.in.:
- java.lang.Runnable
- java.awt.event.ActionListener
- java.lang.Comparable<T>
JAVA.UTIL.FUNCTION:
Słowa kluczowe:
Consumer – przyjmuje, nie zwraca (konsument)
Function – przyjmuje i zwraca (funkcja, działanie)
Predicate – przyjmuje i zwraca boolean (predykat, test logiczny)
Supplier – nie przyjmuje, daje (dostawca)
…Operator – przyjmuje i zwraca ten sam typ (operator, obsługiwacz)
Unary, Binary (Bi) – jednoargumentowy, dwuargumentowy
Double, Int, Long, Boolean, Obj – typy
Interface (@FunctionalInterface) | Description | Przykład |
---|---|---|
Consumer<T> void accept(T t); |
Reprezentuje jednoargumentowego konsumenta (odbiorcę) – działanie, które przyjmuje jeden argument i nie zwraca wyniku. (inaczej: wykonuje bezwynikową operację na wartości typu T)
|
T->void
metoda: accept(Object)
np.: formatujemy tekst na małe literki + .txt Consumer<String> formatuj=text->System.out.println(text.toLowerCase()+”.txt”); formatuj.accept(„pLiK”); //wynik: plik.txt |
DoubleConsumer IntConsumer LongConsumer |
przyjmuje 1 argument typu double (int, long) i nie zwraca wyniku | double->void, int->void, long->void metoda: accept(double), accept(int), accept(long)np.: formatujemy liczbę int przy wydruku IntConsumer formatuj=liczba->System.out.println(liczba+” zł”);formatuj.accept(500); //wynik: 500 zł |
BiConsumer<T,U> |
Reprezentuje funkcję, które przyjmuje dwa argumenty i nie zwraca wyniku.
|
(T,U)->void metoda: accept(Object, Object) |
Function<T,R> { R apply(T t); } |
Reprezentuje funkcję, która przyjmuje jeden argument i zwraca wynik (dokonuje przekształcenia wartości typu T na wartość typu R).
|
T->R metoda: apply(Object) np.: odwrotność liczby typu long: Function<Long, Double> f = x ->1.0/x System.out.println(f.apply(4L)); //wynik: 0,25 |
DoubleFunction<R> IntFunction<R> LongFunction<R> |
Reprezentuje funkcję, która przyjmuje jeden argument typu double i zwraca wynik.
|
double->R, int->R, long->R
metoda: apply(double), apply(int), apply(long)
|
BiFunction<T,U,R> R apply(T t, U u); |
Reprezentuje funkcję, która przyjmuje dwa argumenty różnych typów i daje wynik innego typu.
|
(T,U) -> R
metoda: apply(Object, Object)
np.: sklejamy tekst spacją: BiFunction <String, String, String> f=(x,y) -> x+” „+y;
System.out.print(f.apply(„A”, „LA”) //wynik: ALA
|
Predicate<T> boolean test(T t); |
Reprezentuje predykat* unarny.
Test logiczny dla jednego argumentu (dla wartości typu T) |
T->boolean metoda: test(Object) np.: sprawdzamy występowanie znaku w stringu Predicate <String> f=x->x.contains(„u”); System.out.println(f.test(„Kura”)); //wynik: true |
DoublePredicate IntPredicate LongPredicate |
Test logiczny dla jednego argumentu typu: double (int, long). | double->boolean, int->boolean, long->boolean metoda: test(double), test(int), test(long), |
BiPredicate<T,U> |
Reprezentuje predykat* (wartość funkcji logicznej) dwuargumentowy (podanych dwóch typów).
|
(T,U)->boolean
metoda: test(Object, Object)
np.: testujemy czy 1 argument jest mniejszy od 2 BiPredicate<Integer, Integer> f=(x,y) -> x<y;
System.out.print(f.test(2, 5); //wynik: true
|
Supplier<T> T get(); |
Reprezentuje dostawcę wyników. Nie przyjmuje argumentów ale zwraca wynik (obiekt klasy T). (inaczej: dostarcza bezwarunkową wartość typu T)
|
( )->T
metoda: get( );
|
DoubleSupplier IntSupplier LongSupplier |
Reprezentuje dostawcę wyników typu: double, int, long.
|
( )->double, ( )->int, ( )->long |
BooleanSupplier |
Reprezentuje dostawcę wartości wyników logicznych.
|
()->boolean
metoda: getAsBoolean( )
|
UnaryOperator<T> |
Reprezentuje funkcję operującą na jednym argumencie, dając wynik tego samego typu.
|
T->T
metoda: Function.apply(Object) UnaryOperator<String> f=s->s.toUpperCase();
System.out.println(f.apply(„Koc”)); //wynik: KOC
|
BinaryOperator<T> extends BiFunction<T,T,T> |
Reprezentuje funkcję działającą na dwóch argumentach tego samego typu i tworzącą wynik tego samego typu.
|
(T,T)->T
metoda: BiFunction.apply(Object, Object) BinaryOperator<Integer> f=(x,y) -> x*y;
System.out.print(f.apply(2, 5); //wynik: 10
|
DoubleBinaryOperator IntBinaryOperator LongBinaryOperator |
Reprezentuje operację na dwóch argumentach typu: double, int, long i dającą wynik tego samego typu. | |
DoubleUnaryOperator IntUnaryOperator LongUnaryOperator |
Reprezentuje funkcję na jednym argumencie typu: double, int, long i zwracającą wynik tego samego typu. | |
DoubleToIntFunction DoubleToLongFunction |
Reprezentuje funkcję przyjmującą jeden argument typu double i zwracającą wynik typu int (long). double->int, double->long metoda: applyAsInt(double), applyAsLong(double) |
|
IntToDoubleFunction IntToLongFunction |
Reprezentuje funkcję przyjmującą jeden argument typu int i dającą wynik typu double (long). | int->double, int->long metoda: applyAsDouble(int), applyAsLong(int) |
LongToDoubleFunction LongToIntFunction |
Reprezentuje funkcję przyjmującą jeden argument typu long i zwracającą wynik typu double (int). | long->double, long->int metoda: applyAsDouble(long), applyAsInt(long) |
ToDoubleFunction<T> ToIntFunction<T> ToLongFunction<T> |
Reprezentuje funkcję jednoargumentową, która daje w wyniku wynik określonego typu: double, int, long.
|
metoda: applyAsDouble(Object), applyAsInt(Object), applyAsLong(Object) |
ToDoubleBiFunction<T,U> ToIntBiFunction<T,U> ToLongBiFunction<T,U> |
Reprezentuje funkcję dwuargumentową, która daje w wyniku wynik określonego typu: double, int, long. | metoda: applyAsDouble(Object, Object), applyAsInt(Object, Object), applyAsLong(Object, Object) |
ObjDoubleConsumer<T> ObjIntConsumer<T> ObjLongConsumer<T> |
Reprezentuje operację przyjmującą dwa argumenty typu: Object i double (Object i int, Object i long) i nie zwraca wyniku.
|
metoda: accept(Object, long) |
*Predykat (orzecznik) to funkcja orzekająca o spełnieniu jakiegoś warunku (sprawdza go i zwraca prawdę bądź fałsz). Predykaty mogą być jednoargumentowe (unarne), na przykład funkcja sprawdzająca, czy liczba jest parzysta, lub dwuargumentowe (binarne), przykładowo funkcja sprawdzająca, czy podane argumenty są sobie równe.
Materiały:
http://pracownik.kul.pl/files/10382/public/W13.pdf
https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
http://torun.jug.pl/materials/meetings/1/Wprowadzenie_do_wybranych_zagadnien_JDK_8_by_Szymon_Stepniak.pdf