niedziela, 11 sierpnia 2013

Covariance i Contravariance dla prostych ludzi jak ja

Temat jest bardzo ciekawy ale przy pierwszej styczności istnieje duże prawdopodobieństwo przepalenia zwojów mózgowych jeśli nie byłeś/byłaś nań wcześniej eksponowany. Warto jednak przyjrzeć się mu bliżej gdyż występuje kilku relatywnie nowych językach ale trudno napotkań nań w Javie - przez co znowu ludzie mogą pewnego dnia zostać zaskoczeni nowtym ficzerem Javy 13 podczas gdy cały świat na około będzie miał już to od dawna. Także nie ma co czekać.

W Javie relacje pomiędzy typami prostymi i typami wyższego rzędu możemy skonfigurować tylko w jeden sposób - brak jakiejkolwiek relacji - to się nazywa Invariance . Możliwe są jeszcze inne typy zależności

  • Covariance gdzie List[Object] jest faktycznie rodzicem List[String]
  • Contravariance gdzie List[Object] jest - uwaga !- dzieckiem List[String] co jest trochę na pierwszy rzut oka bez sensu ale w sumie bezsens wynika z napromieniowania mózgu imperatywną Java bo w przypadku funkcji covariance szybko staje się naturalne.

Oczywiście jeśli ktoś zarabia na kolejną ratę kredytu pisząc w Javie i mu z tym dobrze może wyrazić wątpliwość : "ale po co mi to? Ja nie chcę scali!"

Już nie tylko scala

Otóż sytuacja trochę zmieniła się od czasu gdy ten artykuł powstał w 2013 .Gdy go aktualizuję za oknem jest maj 2016 a świat ujrzała pierwsza oficjalna wersja języka Kotlin. Kotlin jest produktem JetBrains - firmy powszechniej kochanej i uwielbianej za ich IDE (to bez ironii - Intellij jest bardzo dobre). Ponieważ owa firma słucha developerów i dopasowuje się w ich gusta od kilkunastu lat to można śmiało stwierdzić, że wiedzą co robią.

No i jak tak spojrzymy na składnię Kotlin-a - Kod wprost z dokumentacji to naszym oczom ukaże się :

val a: Int = 10000
print(a === a) // Prints 'true'
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA === anotherBoxedA) // !!!Prints 'false'!!!
Ten kod tak w 90% wygląda jak Scala... Tak drogi czytelniku - firma, która od kilkunastu lat robi jedno z lepszych IDE dla programistów Javy i zna ten język an wylot. Z jakiegoś powodu postanowiła zerwać z dotychczasowa składnią Javy na rzecz składni "scalopodobnej".

Ale co ważne w kontekście tego artykułu to informacja, że i tam pojawia się jakaś forma contravariance i covariance. Może więc czas obok funkcji praktycznej zauważyć funkcję edukacyjna nowych języków? Niestety wygląda na to, że Java pomimo swej dojrzałości zostaje w tyle za innym językami i warto zadbać o swoją edukację "poza Javą".

A oto link do przykładu : https://kotlinlang.org/docs/reference/generics.html#declaration-site-variance

abstract class Source<out T> {
  abstract fun nextT(): T
}

abstract class Comparable<in T> {
  abstract fun compareTo(other: T): Int
}

Poniżej dla porównania kawałek .Net. Wygląda podobnie do tego kotlinowego kodu i znów pokazuje, że te koncepcje ogólnie nie są nowe. Nowe sa jedynie dla Javy.

public delegate TResult Func<in T, out TResult>(
    T arg
)

Przykłady obyczajowe

Generalnie aby nauka szła gładko warto sprząc przyswajaną wiedzę z wiedzą już posiadaną. A do tego jeśli owa posiadana wiedza ma charakter towarzysko - obyczajowy to powinno być i pożytecznie i zabawnie. Dlatego też tematem przewodnim w poniższych przykładach będzie wóda.

Ale o soooo chozzii...

W trakcie nauki programowania funkcyjnego tu i ówdzie napotyka się zestaw słówek: contrvariance,contravariance i invariance . W zasadzie to przy okazji lektury Javy też można na nie natrafić - najczęściej przy okazji generyków - ale ponieważ w Javie nie ma czegoś takiego jak declaration site variance dlatego też ów temat schodzi na poboczny tor.

Convariance i Contravariance są szczególnie ważne w kontekście funkcji, które wejście mają contravariant a wyjście covariant - oczywiście funkcje w większości języków bo jest jeden który tego tam nie ma i pozostawię czytelnikom do zgadnięcia który...

Generalnie w Javie można zadeklarować pewne zależności pomiędzy typami a listami jedynie przy deklaracji referencji do listy a nie listy samej w sobie.

List<String> strings=new LinkedList<>();
//List<Object> objects=strings; //compilation error

List<? extends Object> objectsAndChilds=strings;

Podobnego problemu nie ma Scala w przypadku listy "immutable".

val strings:List[java.lang.String]=List("1","2","3")
val objects:List[java.lang.Object]=strings

Jak i w kotlinie możemy to zrobić bezproblemowo

val list: List<String> =listOf("1","2","3")
val anys:List<Any> = list

Gdzież jest źródło tego oto zachowania?

Deklaracje

Deklaracja Listy w Scali i w Kotlinie mają obok generyka dodatkowe znaczki.

sealed abstract class List[+A] 
public interface List<out E> 

Własnie owe magiczne znaki sprawiają, że zachodzi odpowiednia relacja pomiędzy listą a przechowywanym wewnątrz jej typem. Warunek : Lista musi być "po angielsku immutable". Jeśli to był szok to co się stanie gdy zerkniemy do funkcji?

trait Function1[-T1,+R]

O ile Covariance (to z plusem) jeszcze jest jakoś intuicyjne o tyle Contravariance(to z minusem) już może czesać bańke. Dlatego też zilustrujemy mechanizm przykładem (obyczajowym)

Przyklad życiowy

Poniżej hierarchia klas, która posłuży nam do wytłumaczenia tematu. Domeną jest zestaw młodego chemika-smakosza :

class Alkohol
class Metanol extends Alkohol
class Etanol extends Alkohol
class Browar extends Etanol
class RozwodnionyBrowar extends Browar
class Wóda extends Etanol

Teraz jeszcze jakie haj order funkszyn w kontekście sklepu osiedlowego :

class SklepOsiedlowy{
 val browar=new Browar

 def realizujUsluge(usluga:Browar => String) = usluga(browar)
}

I nadchodzi teraz Clu wpisu na którym swego czasu zdzierałem sobie zwoje. Pamiętając, że cały czas obowiązuje zasada, iż do funkcji można przekazać albo deklarowany typ albo coś co z niego dziedziczy (jak w Javie) to które funkcje z poniższych można przekazać do "sklepu osiedlowego"?

val uslugaSpozywcza=(napój: Browar)=> "piję "+ napój //bramka nr 1
val kiepskaUslugaSpozywcza=(rozwodnionyNapój: RozwodnionyBrowar)=> "piję "+rozwodnionyNapój //bramka nr 2
val generycznaUslugaSpozywcza=(jakiśNapój: Alkohol) => "piję "+jakiśNapój //bramka nr 3

Pierwszy mindfuck przez jaki musiałem przebrnąć to "czucie" funkcji jako typu. Po latach siedzenia w Javie mój mózg cały czas próbuje widzieć je jako metody a nie dane same w sobie i cała koncepcję funkcji sprowadza do wywołania. Na szczęście znajomość javascriptu trochę tu pomaga ( ale i trochę przeszkadza). W każdym razie sklep osiedlowy ma metodę, która przyjmuje funkcję typu : TYP na wejściu Browar , TYP na wyjściu String. Na pewno do tego typu pasuje uslugaSpozywcza bo na wejściu ma Browar a na wyjściu String.

I właśnie tutaj pojawia się arcy ciekawe pytanie: która funkcja jest podtypem funkcji uslugaSpozywcza ? Paradoksalnie okazuje się, że podtypem jest... generycznaUslugaSpozywcza - tak to co przyjmuje "nadtyp" - jako funkcja staje się "podtypem".

Skąd ten potencjalny mindfuck? Być może w mózgu dzieje się coś takiego : jak RozwodnionyBrowar jest podtypem Browaru to zwoje kory szarej chcą aby funkcja, która przyjmuje RozwodnionyBrowar była tez podtypem funkcji, która przyjmuje Browar - ale jest odwrotnie! - dlatego pewnie nazywa się to CONTRAvariance (i ten minusik trait Function1[-T1,+R]).

Wytłumaczenie - podejście pierwsze

Wróćmy do kawałka kodu :

val browar=new Browar
def realizujUsluge(usluga:Browar => String) = usluga(browar)

A w szczególności tego fragmentu usluga(browar). Generalnie chodzi o to, żeby funkcja jakiej tutaj użyjemy mogła przyjąć TYP Browar.

  • val uslugaSpozywcza=(napój: Browar)=> "piję "+ napój - MOŻE
  • val generycznaUslugaSpozywcza=(jakiśNapój: Alkohol) => "piję "+jakiśNapój - MOŻE BO BROWAR TO ALKOHOL
  • val kiepskaUslugaSpozywcza=(rozwodnionyNapój: RozwodnionyBrowar)=> "piję "+rozwodnionyNapój - NIE MOŻE BO BROWAR TO NIEKONIECZNIE ROZWODNIONY BROWAR

Wytłumaczenie - podejście Drugie

Dla odmiany inna hierarchia

    class Kulturysta
    class Koksu extends Kulturysta
    class KoksuZMikrofonem extends Koksu

    val napierdalanie=(k: Kulturysta)=>"napierdalanie"
    val wywiady=(k:Koksu)=>"wywiady"
    val koksuSpiewaZgwiazdami=(k: KoksuZMikrofonem)=>"spiewanie"

    val telewizja = (fk:Koksu=>String)=>"dzisiaj w  telewizji" + fk(new Koksu)

    telewizja(wywiady)                                //> res1: String = dzisiaj w  telewizji wywiady
    telewizja(napierdalanie)                          //> res2: String = dzisiaj w  telewizji napierdalanie
    //telewizja(koksuSpiewaZgwiazdami) - to się nie kompiluje

Mamy tutaj standardowego koksa jako argument przekazywany do funkcji, która to funkcja (z racji tego, że sama jest typem) została przekazana do funkcji wyższego rzędu. Nasz standardowy koks może wziąć udział w wywiadach bo ta funkcja oczekuje właśnie koksa. Może wziąć udział w napierdalaniu (czyli ćwiczeniach), gdyż ta funkcja oczekuje dowolnego kulturysty czyli jest podtypem każdej funkcji, która przyjmuje koksa bo można ją zawsze przekazać w jej miejsce. Ostatnia funkcja oczekuje specyficznego koksa z mikrofonem i tutaj niestety kod się nie kompiluje.

A teraz to drugie z plusem

Teraz druga część - COVARIANCE. To jest bardziej intuicyjne bo tutaj typy idą niejako w tym samym kierunku co funkcje : trait Function1[-T1,+R]). Ponownie użyjemy znanej struktury typów

class Alkohol
class Metanol extends Alkohol
class Etanol extends Alkohol
class Browar extends Etanol
class RozwodnionyBrowar extends Browar
class Wóda extends Etanol

I do tego metodki :

val generycznaReceptaSpozywcza=()=>new Etanol
val receptaNaBrowar= ()=> new Browar
val receptaNAWódęZMety=() => new Metanol

class Wytwornia{
 def realizuje(recepta:() => Etanol) = recepta()
}

Mamy klasę wytwórnia, która spodziewa się bezparametrowej funkcji zwracającej Etanol. Czyli na bank do wywołania pasuje generycznaReceptaSpozywcza. No i dalej już powinno być jaśniej, że nie można tykać metody z Metanolem bo ludzie poumierają. Idzie taki Zdzisław do sklepu i zamiast produktu do konsumpcji spożywczej dostaje paliwo rakietowe (nie mylić z rocket fuel). Ale jak zamiast dowolnego Etanolu dostanie Browara to jest ok. Czyli tutaj funkcja, która jest podtypem również zwraca podtyp - to powinno być łatwiejsze do zrozumienia.

Post Scriptum o przykładzie

Pomimo, że przykład alkoholowy używa mainstrimowej analogii to jednocześnie pragnę przypomnieć, że alkohol w dużych dawkach jest niezdrowy.

Dalsza Lektura

Doskonałą lekturą uzupełniającą jest tenże link == o tutaj ==> Covariance_and_contravariance_(computer_science). I chociaż czasem ludzie podchodzą trochę nieufnie do artykułów na wikipedii to jednak informacje techniczne moim zdaniem są tam umieszczony w sposób obiektywny, zorganizowany i łatwy w nawigacji.

Brak komentarzy:

Prześlij komentarz