Mroźnym popołudniem standardowego dnia pracy wiedzeni drzemiącą w nas naturą podróżnika możemy wylądować gdzieś w pośród deklaracji metod bibliotecznych, które w javie8 wielce prawdopodobne będa przyjmować funkcje. I wtedy to naszym oczom może ukazać się coś takiego :
Function<? super V, ? extends T> declarationName
I tak w zasadzie to podobne rzeczy będą się ukazywać nam co chwilę... coraz częściej... w zasadzie w przypadku funkcji inna deklaracja(bez tego super i extends) nie ma sensu. Omówimy sobie skąd się to wzięło, dlaczego tak zostało, czemu inne deklaracje nie mają sensu i dlaczego pomimo, że to jedyna deklaracja z sensem to trzeba z palca zawsze to extends i super dodawać oraz, że istnieje alternatywa którą (znowu) ma w zasadzie wiele języków a nie ma java.
Problemy z typem typu
Dawno dawno temu wszystko w Javie było faktycznie obiektem. Nawet jak nie chciałeś. Jeśli mieliśmy Listę to mieliśmy listę obiektów. Były to czasy gdy monitory CRT wypalały oczy a aplikacja startowała 15 minut po to by zaraz po starcie rzucić wyjątek "class case exception" - no bo trzeba było zgadywać co za typ kryje się za obiektem. Nie było generyków.
Gdy generyki w końcu zawitały do Javy5 te problemy stały się znikome ale fani zbyt uproszczonych rozwiązań dostali nowe tematy do hejtowania bo oto pojawiły się pewne zachowania z punktu widzenia bazującego na "wszystko jest obiektem" - trochę nieintuicyjne.
No bo mamy takie coś, że -> String jest Obiektem -> mamy listę stringów -> List
List<String> stringi=new LinkedList<>(); List<Object> objjjekty = stringi; objjjekty.add(1); //kupa
A ponieważ doprowadziłoby to do eksplozji to takie niecne programowanie jest zabronione. No ale jeśli dobrze nam z oczu patrzy i obiecamy, że nie będziemy tam nic wkładać to kompilator może nam pójść na pewne ustępstwo :
List<? extends Object> probaDruga=stringi;I jest teraz zabezpieczenie w postaci dziwnego typu co wystarczy by nas powstrzymać przed wrzucaniem tam śmieci
Czy można było inaczej to zrobić? Można było, co zobaczymy później ale patrząc na ten kawałek kodu z listą wydaje się, że mechanizm jest w porządku, nieprzekombnowany i ratuje nas przed ClassCastEx. To był rok 2004. Mija kolejnych 10 lat. Cała dekada. Wychodzi Java8 a w niej Funkcje . I to właśnie ten nowy mechanizm pokaże słabe strony wyborów z przeszłości...
Use Site Variance
Jeśli wejdziesz w definicję java.util.function.Function to zobaczysz do czego te zabawy z generykami doprowadziły
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); }
Pytania czy te "extends" i "super" musza tam być? To co teraz zrobię być może ma jakąś nazwę w nauce o wnioskowaniu (a może nie) ale generalnie one tam być muszą bo... nie ma żadnych argumentów za tym aby w przypadku funkcji ich tam nie było! (ewentualnie ktoś może mnie tutaj do-edukować).
Extends
Jaka jest różnica pomiędzy dwoma poniższymi funkcjami bibliotecznymi? (taka funkcja "map" dla ubogich)
public static <A,B> Collection<B> libraryMethod(Collection<A> c, Function<A,B> f){ List<B> l =new ArrayList<>(); for(A a: c){ l.add(f.apply(a)); } return l; } public static <A,B> Collection<B> libraryMethod2(Collection<A> c, Function<? super A,? extends B> f){ List<B> l =new ArrayList<>(); for(A a: c){ l.add(f.apply(a)); } return l; }
Różnica pojawi się w wywołaniu bo mając klasę
class User{ private String name; private Integer age; public User(String name, Integer age) { this.name = name; this.age = age; } public String getName() { return name; } public Integer getAge() { return age; } }
Chciałbym teraz wykonać poniższe przekształcenie:
Collection<User> users=new LinkedList<>(); Function<User,String> display=o->o.toString(); Collection<Object> res1=libraryMethod(users,display); // error Collection<Object> res2=libraryMethod2(users,display);
Ale nie można go wykonać :( Wiem, że w tych akapitach zbytnio się nie rozpisałem ale próbki kodu chyba lepiej pokazują o co chodzi. Bez dodatkowego wysiłku i dodatkowych deklaracji wprowadzamy niczym nie uzasadnione ograniczenie. Nie ma naprawdę żadnego argumentu za ograniczeniem przyjmowanych funkcja do stricte typów biorących udział w równaniu gdyż nic przez to nie zyskujemy a tracimy wszystkie przypadki wywołania gdy typy nie zgadzają się "jeden do jednego". I w zasadzie tak będzie wszędzie - w sensie każdej funkcji bibliotecznej. Nie potrafię znaleźć kontr-przykładu.
Super
Z tym super na początku sprawa jest trudniejsza bo to szalenie nieintuicyjne co tu się dzieje. Starałem się to kiedyś wytłumaczyć tutaj -> http://pawelwlodarski.blogspot.com/2013/08/covariance-i-contravariance-dla.html. Tutaj spróbujmy taki prosty przykład. Niech User ma podklasę jakaś taką specjalną :
class SpecialUser extends User{ private String somethingSpecial; public SpecialUser(String name, Integer age, String somethingSpecial) { super(name, age); this.somethingSpecial = somethingSpecial; } }
Mamy znowu inna funkcję biblioteczną, która tym razem filtruje nasze obiekty :
public static <A> Collection<A> filter1(Collection<A> c, Function<A,Boolean> f){ List<A> l =new ArrayList<>(); for(A a: c){ if(f.apply(a)) l.add(a); } return l; } public static <A> Collection<A> filter2(Collection<A> c, Function<? super A,Boolean> f){ List<A> l =new ArrayList<>(); for(A a: c){ if(f.apply(a)) l.add(a); } return l; }
I znowu nie ma żadnego powodu by nie pozwolić tej metodzie przyjmować funkcje, które działają na podtypach bo przecież jest to całe is-A zapewnione - w sensie, że w podtypie na pewno znajdą się te metody których wymaga metoda działająca na rodzicu i całość się skompiluje.
Function<User,Boolean> isAdult=user->user.getAge()>= 18; Collection<SpecialUser> specialUsers=new LinkedList<>(); // filter1(specialUsers,isAdult); //error filter2(specialUsers,isAdult);
No i oczywiście jak zajrzysz w implementacje java.util.stream.Stream to i tam też map oraz filter również mają te super i extends -> bo zwyczajnie inna deklaracja nie ma sensu! Ale skoro nie ma sensu to czy dałoby się zrobić to jakoś inaczej by tyle tego nie pisać i by uprościć typy? Oczywiście, gdyż to już jest, to już jest, jest już to zdrowo i wesoło we wszystkich językach w koło
Declaration-Site Variance
Otóż w językach innych możemy zamiast pisać po tysiąc razy to "extends" zwyczajnie napisać jeden raz przy deklaracji, domyślnie zawsze będzie "extends", że to jest po prostu natura danej konstrukcji.
C#interface IProducer<out T> // Covariant { T produce(); } interface IConsumer<in T> // Contravariant { void consume(T t); }Kotlin
abstract class Source<out T> { abstract fun nextT(): T }Scala
trait Function1[-T1,+R] extends AnyRef
To dlaczego nie tak?
Dlaczego w javie jest to zrobione ala "use-site variance?" . Najprawdopodobniej gdy ten mechanizm powstawał wydawał się doskonałym pomysłem bo java miała jedynie mutowalne kolekcje do których ta maszynka wystarczała. Przypomnijmy - były to lata 2002-2005 -> IE miało 90% udziałów w rynku, ratunku dla branży widziano w tonach xmli a o funkcjach nikt nie myślał. No i w Javie musi być kompatybilność wstecz to jak już zrobili to tak zostało... Jeśli ktoś szuka przykładu długu technicznego to wydaje mi się, że tutaj właśnie go znaleźliśmy.
O nielicznych zaletach i licznej krytyce "Use-Site variance" przeczytacie tutaj -> https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)#Comparing_declaration-site_and_use-site_annotations. Generalnie jeśli chodzi o funkcje to przewaga "declaration-site variance" wydaje mi się bezdyskusyjna. Bardziej problematyczne są kolekcje. O ile chyba Kotlin ma obydwa typy podejść o tyle np. (znowu z tego co mi wiadomo) scala zrezygnowała z "use-site" na jakimś tam początkowym etapie rozwoju gdyż niewiele to dawało a utrudniało mocno koncepcję auto wykrywania typów dzięki któremu programy w scali mają tone mniej tekstu niż to co reprezentuje sobą java. Zaś co do samych kolekcji należałoby przenieść dyskusje na zupełnie inną płaszczyznę zatytułowaną "algorytmy z wykorzystaniem kolekcji niezmiennych" a tu już jesteśmy o krok od FP vs OOP co może nas doprowadzić do ogólnego wniosku, że problemy z generykami w Javie są pochodną wybranego paradygmatu programowania.
Pocieszenie
Jeśli pracujesz w dobrym ogromnym korpo gdzie kastą rządząca są ludzie biegający z plikiem exceli pod pachą (tzw. "jego EXCELencja") to nie masz się czym martwić bo i tak javy8 pewnie nie zobaczysz przez następne 10 lat. Także głowa do góry!
linki
- https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)
- https://kotlinlang.org/docs/reference/generics.html#declaration-site-variance
- https://schneide.wordpress.com/2015/05/11/declaration-site-and-use-site-variance-explained/
- https://medium.com/byte-code/variance-in-java-and-scala-63af925d21dc#.lleehih3p
Tyle pytań rekrutacyjnych o PECS... I wszystko jak krew w piach ;)
OdpowiedzUsuńGenialne pocieszenie na końcu ;) Śmiechłem z tego bardzo :D
OdpowiedzUsuń