Interfejsy funkcyjne, lambda-wyrażenia

Filed in KomputerowoTags: , , , , ,

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)
np.: zamiana tekstu na wersaliki

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)
np.: wyliczamy iloczyn 2 liczb

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