Scala - parametry domniemane - motywacja

0

Po dłuższej przerwie wracam do nauki Scali. Może pytanie, które zadam jest po prostu jest głupie i równoważne z tym "po co zostały wprowadzone pętle", ale zastanawia mnie po co (jaka motywacja za tym stoi) wprowadzone zostały "parametry domniemane" (implicit parameters)?

Z przykładów praktycznych to przewija się automatyczna konwersja typów, wstrzykiwanie metod czy zależności. Natomiast nie wiem czy przykłady wynikają z tego, że najpierw pojawiły się parametry domniemane i przykłady są konsekwencją, czy może odwrotnie, najpierw pojawiły się problemy (będziemy potrzebować automatycznej konwersji typów) i wymyślono ten mechanizm, który ma również inne zastosowania.

1

Hej,
ja myślę, że to trudne pytanie... Generalnie twórca Scali (Martin Odersky)... wprowadził pewne techniki umożliwiające domyślne działania funkcji lub jej konwersję w zależności od kontekstu... Między innymi umożliwia mu to korzystanie ze wszystkich dobrodziejstw Javy, ale można tworzyć też własne Scalowe odpowiedniki... Myślę, że także dzięki tej elastyczności można pracować na objektach Sparka jak na zwykłych objektach Scalowych, wykorzystując też przy tym Scalowe funkcje, np. filter, itp... Ale w Pythonie "konwersje" chyba robi się łatwiej:

def f(x):
    try:
       if type(x[0])=='int':
            return sum(x)
	   else:
	        return list(x)
    except:
        print("think a little about it")

Poczytaj sobie może to: http://www.lihaoyi.com/post/ImplicitDesignPatternsinScala.html Może ciut trywializuję problem... Pozdrawiam... :)

2

Parametry implicit nie mają nic wspólnego z konwersjami typów. Są uogólnieniem wzorca type-classes z Haskella.
Są zaawansowaną formą programowania generycznego. Umożliwiają rozwiązanie choćby tzw. expression problem (https://en.wikipedia.org/wiki/Expression_problem).
W praktyce przekłada się to na możliwość pisania kodu, który łatwo rozszerzać o obsługę nowych typów bez konieczności modyfikacji istniejącego kodu.

Przykład: biblioteka standardowa wie jak posortować Stringi i wie jak posortować Inty. Jednak Ty chcesz wprowadzić własny typ Z, który też chcesz aby był sortowalny. Dzięki implicitom możesz to zrobić w taki sposób, że Twój typ będzie akceptowany we wbudowanych metodach sortBy, min, max, minBy wymagających porównywania elementów itp. Co więcej, dzieki implicitom np. krotka (Int, Int, Z) też będzie wtedy sortowalna. W słabszych językach (Java, Kotlin, C#) takie coś też da się do pewnego stopnia uzyskać korzystając z dziedziczenia i interfejsów, ale jest mocno ułomne.

A konwersje typów to raczej jakiś niezamierzony efekt uboczny implicitów. Nie należy tego tak używać.

0

Dzięki za linki, które sporo wyjaśniają i co dla mnie istotne, dają nowe spojrzenie.

Zastanawia mnie jeszcze jedno, czy w takim razie mixiny nie są w pewien sposób redundantne jeśli dysponujemy funkcjonalnością parametrów domniemanych?

1

Może pytanie, które zadam jest po prostu jest głupie i równoważne z tym "po co zostały wprowadzone pętle", ale zastanawia mnie po co (jaka motywacja za tym stoi) wprowadzone zostały "parametry domniemane" (implicit parameters)?

Parametry implicit pojawiły się w którejś wersji Scali przy okazji przerabiania hierarchii kolekcji. Wprowadzono wtedy domniemany parametr CanBuildFrom, który służył do wielu rzeczy związanych z kolekcjami. Potem zauważono, że parametry implicit mają wiele innych zastosowań i można nimi emulować wiele konstrukcji znanych z innych języków (jak np wspomniane tutaj typeclasses z Haskella).

Zastanawia mnie jeszcze jedno, czy w takim razie mixiny nie są w pewien sposób redundantne jeśli dysponujemy funkcjonalnością parametrów domniemanych?

Trait ma dostęp do parametrów o widoczności protected (i może takie zawierać), obsługuje wielodziedziczenie poprzez linearyzację traitów (to rozwiązuje diamond problem) i może mieć składowe abstrakcyjne (niezaimplementowane). Zależnie od stylu programowania bardziej przydatne mogą być albo mixiny albo implicit parametry.

0

Patrząc teraz od strony obiektowej, to wydaje mi się że jest to podobne do wyrzucenia na zewnątrz klasy pewnej logiki:

Pseudo-java-kod:

interface ExternalLogic<T> {
	boolean evaluateFoo(T x);
}
...
MyClass(Param param, ExternalLogic<T> externalLogic) 	{
	this.externalLogic = externalLogic;
	this.param = param;
}

// Uzywa externalLogic do podjecia decyzji 
Object doSth()  {
	...
}
...

Gdyby w Scali nie było "implicit", to nadal można by korzystać z tego wzorca, tylko (albo "aż") ręcznie uzupełniać "implicitParam" (tak, wiem, wtedy byłby "explicit" ;-) ), ewentualnie stworzyć sobie mechanizm, który nas od tego uwolni.

Jeśli dobrze zrozumiałem, to w Scali jest "bogatsza" realizacja takiego wzorca, gdyż:

  • błędy zostaną wykryte na etapie kompilacji
  • parametr domniemany zostanie "wstrzyknięty" (czyli tak jakbyśmy mieli funkcjonalność wstrzykiwania zależności)
  • kod będzie zwięzły
  • mechanizm znajduje się w standardzie języka
0

Wstrzykiwanie będziesz miał jak wstawisz słówko implicit obok parametru konstruktora. Zwykle robi się wtedy dwie listy parametrów w konstruktorze, np:

class Klaska(parametrJawny: A)(implicit parametrDomniemany: B) {
 // (...)
}

Parametry implicit skracają kod. Czasami mocno, bo argumentem implicit może być wynik metody, która sama ma parametry implicit.

Dotty, czyli rozwojowa wersja Scali 3 idzie z implicitami jeszcze dalej: http://dotty.epfl.ch/docs/reference/implicit-function-types.html

1

Gdyby w Scali nie było "implicit", to nadal można by korzystać z tego wzorca, tylko (albo "aż") ręcznie uzupełniać "implicitParam"

Cała magia implicitów polega na tym, że to automatyczne uzupełnianie odbywa się na podstawie typu, a sam proces dopasowywania obiektu do typu może przebiegać wg dowolnego algorytmu w trakcie kompilacji (implicity są kompletne w sensie turninga, a ponadto mogą korzystać z makr). Innymi słowy, nie ma praktycznie ograniczenia na to, jak skomplikowany będzie proces znajdowania odpowiedniego obiektu. Możesz napisać sobie makro dostarczające implicit a w tym makrze 10k linii. W efekcie to "tylko" ręczne wstrzykiwanie to może być jednak bardzo duża niedogodność w porównaniu z automatycznym.

Dzięki implicitom możesz robić takie fajne rzeczy jak np. transformowanie obiektów do/z JSON bez używania refleksji.

Implicitów używa się zresztą nie tylko do wstrzykiwania, ale też do nakładania złożonych ograniczeń na typ. Przykładowo, można w Scali napisać metodę, którą da się wywołać (na etapie kompilacji) tylko dla obiektu, który nie jest określonego typu. Z każdym typem będzie działać, a z jednym wybranym nie. Zrobisz takie coś w OOP i zwykych generykach Java/C#? ;) Może przykład trochę zbyt teoretyczny, ale ogólnie mechanizm ten umiejętnie wykorzystany, pozwala programować w sposób znacznie bezpieczniejszy niż zwykłe OOP. W wielu sytuacjach bez implicitów musiałbym rzucać wyjątkami. Błąd na etapie kompilacji jest lepszy niż błąd w trakcie działania.

Kolejnym sztandarowym wręcz zastosowaniem implicitów (wraz z typami wyższych rzędów) jest realizacja metod zwracających obiekt, którego typ jest zależny od typu parametrów wejściowych. Np. transformujesz kolekcję i w wyniku chcesz dostać kolekcję tego samego typu. Spróbuj w klasycznym programowaniu obiektowym napisać uniwersalną metodę, która tworzy nową kolekcję tego samego typu, ale z inną zawartością. W praktyce mógłbyś:

  • powielać implementację dla każdego typu osobno (np. osobna implementacja dla zbiorów, osobna dla list), wykorzystując przeciążanie / różne klasy
  • dorzucić ręcznie jakiś parametr realizujący wzorzec "builder", ale wtedy nie wymusiłbyś na użytkowniku, że typ buildera musi odpowiadać typowi kolekcji wejściowej (tj. zapewne wymusiłbyś w runtime i sygnalizował jakimś wyjątkiem - paskudztwo).
0

Na zwykłych generykach pewnie nie, ale korzystając z refleksji myślę, że się da. Na szybko nie przychodzi mi jednak jakieś praktyczne zastosowanie do głowy. Sam mechanizm wygląda na potężny, ale jak to mówią "with great power comes great responsibility", pole dla antwzoróców wydaje się duże :)

Póki co jestem na etapie rozpoznawania mechanizmów w scali, ale myślę, że za jakiś czas docenię dobrodziejstwa z tego płynące.

0

Trochę pobocznie do głównego tematu, bo takie gdybanie, a nie pytanie jak jest :)

Twórca Erlanga pisał kiedyś o językach OO: "The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle."

Czy nie jest to jednak takie zatoczenie koła przez twórców Scali, która wydaje mi się, że dodaje to co było/jest mankamentem (wg Joe Armstronga) OO? Owszem, jest "banan, ale potrzebuje mimo wszystko jakiejś dżungli, która pojawi się z kapelusza".

W obliczu braku zgodności wstecznej w kolejnych wersjach może taka rzecz jak implicite zniknąć, czy już za późno? :-)

0

Sam mechanizm wygląda na potężny, ale jak to mówią "with great power comes great responsibility", pole dla antwzoróców wydaje się duże :)

W praktyce rzadko spotykam się z tym by koledzy wrzucali parametry implicit gdzie popadnie. Zdecydowanie częściej dało się uświadczyć dziwaczne funkcje (wyższych rzędów lub nie). (nierealistyczny) Przykład na pokręcone lambdy:

Twórca Erlanga pisał kiedyś o językach OO: "The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle."

Czy nie jest to jednak takie zatoczenie koła przez twórców Scali, która wydaje mi się, że dodaje to co było/jest mankamentem (wg Joe Armstronga) OO? Owszem, jest "banan, ale potrzebuje mimo wszystko jakiejś dżungli, która pojawi się z kapelusza".

W obliczu braku zgodności wstecznej w kolejnych wersjach może taka rzecz jak implicite zniknąć, czy już za późno? :-)

Trzeba się zorientować o co chodziło Armstrongowi. Być może chodzi np o encje Hibernate'owe gdzie otrzymując obiekt bazodanowy otrzymujesz także persistence managera, połączenie do bazy, dirty state i cuda nie widy. W Scali generalnie nie ma takich dziwactw. W Scali implicity służą raczej do ad-hoc polymorphism, chociaż czasami też do przesyłania kontekstu. Tyle, że jeśli masz implicit Clock czy ExecutionContext (pulę wątków) to tak naprawdę jest lepiej niż gdybyś miał domyślny kontekst zaszyty w Thread Local Storage albo wstrzykiwany magiczną biblioteką. Innymi słowy - parametry implicit to dodatkowe banany, a nie dodatkowe goryle :] Pochodzenie implicitów możesz też sprawdzić nawet przed kompilacją (w IntelliJu skrót Ctrl+Alt+P i Ctrl+Alt+Q), natomiast obecność goryla z Hibernate'a czy magicznego kontenera DI sprawdzisz dopiero w czasie wykonania.

Implicity nie znikają, raczej idzie to w drugą stronę. W Dotty (który ma być docelowo Scalą 3) dochodzą implicit function types - pisałem o tym w tym wątku.

0

Dziękuję wszystkim za dyskusję. Temat implicitów dla mnie wyczerpany, ale wrócę z innymi zagadnieniami :-)

0

@yarel: ale to nie ma nic wspólnego z tym co mówi Joe Armstrong bo AFAIK parametry domniemane nie mogą nieść ze sobą stanu a tylko metody, co oznacza, że to jest bardziej odpowiednik behaviours z Erlanga, czyli sam Erlang również posiada mechanizm, który pozwala na podobne praktyki.

0
hauleth napisał(a):

@yarel: ale to nie ma nic wspólnego z tym co mówi Joe Armstrong bo AFAIK parametry domniemane nie mogą nieść ze sobą stanu a tylko metody, co oznacza, że to jest bardziej odpowiednik behaviours z Erlanga, czyli sam Erlang również posiada mechanizm, który pozwala na podobne praktyki.

Pewnie masz rację, ale scala to też oop, więc czy przekazanie obiektu jako implicit nie oznaczałoby, że stan jednak może być przekazany? Nie próbowałem tego w praktyce.

0

Banana trzymanego przez goryla możesz przekazać zarówno przez parametr jawny jak i przez niejawny. W praktyce jednak parametry niejawne są używane po to, by zmniejszyć ilość goryli. Np zamiast encji Hibernate'owych mamy Slicka wykorzystującego mocno implicity, ale jak dostaniesz ze Slicka obiekty z danymi to one nie mają przypiętego persistence managera, active recorda czy żadnego innego cuda. Dostajesz po prostu niemutowalny obiekt bez niespodzianek w środku. Albo inny przykład: parametry implicit zastępują worki na wszystko typu kontener DI czy service locator.

Parametr niejawny zawsze też możesz podać jawnie jeśli jest zdefiniowany jako parametr implicit (w przypadku context bounds może być wymagana odrobina gimnastyki). Np zamiast:

Future(obliczenia) // domniemana pula wątków

możesz napisać:

Future(obliczenia)(jawnaPulaWątków)

1 użytkowników online, w tym zalogowanych: 0, gości: 1