Nadużywanie scope functions w Kotline

0

Scope functions (let, also, apply, run itd.) są bardzo uniwersalne i można je stosować w aż tak wielu różnych miejscach, że nachodzi pokusa, żeby stosować je niemalże wszędzie gdzie się da, eliminując potrzebę deklarowania zmiennych w kodzie prawie do zera (poza parametrami funkcji, o ile to zaliczamy je do zmiennych) oraz stosując wyłącznie "single-expression functions". Czy w Kotlinie warto zawsze i wszędzie używać scope functions? A może warto ograniczyć ich użycie maksymalnie i stosować np. tylko typowe konstrukcje typu ?.let{ ... }? Zgaduję, że prawda leży gdzieś po środku. Jakie jest wasze zdanie na ten temat? Czy ich stosowanie zwiększa, czy zmniejsza przejrzystość kodu? Czy ułatwia czy utrudnia rozwój? Czy wpływa na niezawodność kodu?

Poniżej kilka przykładowych fragmentów kodu, żebyśmy mogli porozmawiać o tym samym. Proszę nie doszukiwać się w nich jakiegoś głębszego sensu, ani nie sprawdzać, czy w ogóle się skompilują. Chciałbym, żeby uwaga skupiła się na scope functions, a nie na tym, czy w kodzie nie ma błędu.

fun foo() = boolQuery().also { builder ->
    buildFooQuery()
        .takeIf { builder.should().isNotEmpty() }
        ?.let { builder.must(it) }
}

fun bar(phrase: String) = boolQuery().apply {
    minimumShouldMatch(1)
    buildFullTextQueries(phrase).forEach {
        should(it)
    }
}

fun createSomethings(field: Field): List<Something> =
    getSomethingValue()
        .let { it as? List<*> }
        ?.let { buildSomethingComponents(it, somethingMetadata) }
        ?: emptyList()


fun createXyz(fields: List<Field>) =
    somethingBuilder
        .getBuilderForFields(fields)
        .let { buildSomethingComponent(it) }?.attributes
        ?.let { filterSomethingAttributes(fields, it) }

fun filterSomethingAttributes(
    fields: Map<Xyz, Something>,
    somethingAttributes: MutableMap<String, SomethingAttributes>
) = xyzFields
    .mapNotNull { fields[it] }
    .map { it.name() }
    .let { name ->
        if (xyzFields.isNotEmpty())
            somethingAttributes.filterKeys { name.contains(it) }
        else somethingAttributes
    }

fun foo2() = doDoSomething().also{ logSomething("xx: $it") }

fun foo3() = doDoSomething().apply{ build() }.also{ logSomething("xx: $it") }

W ramach ćwiczenia napisałem w takim stylu ok. 1k linijek kodu nie używając ani val, ani var, ani klasycznych funkcji z blokiem kodu, a jedynie "single-expression function". Mam kilka swoich przemyśleń na ten temat, m. in. jaką przewagę ma taki styl pisania, ale wolałbym się tym nie dzielić w pierwszym poście. Czy ten styl można podsumować dwoma słowami: "programowanie funkcyjne"?

0
bustard2 napisał(a):

Jakie jest wasze zdanie na ten temat?

Ja robię "na czuja".

Czy ich stosowanie zwiększa, czy zmniejsza przejrzystość kodu?

To jest kwestia subiektywna.

Czy ułatwia czy utrudnia rozwój?

Chyba ani jedno ani drugie.

Czy wpływa na niezawodność kodu?

Nie.

2

Czy ten styl można podsumować dwoma słowami: "programowanie funkcyjne"?

Nie. Z funkcyjnością ma to tyle wspólnego, co rakieta z rakietą do tenisa.

Nadużywanie jakichkolwiek konstruktów nie jest dobre, trzeba znaleźć złoty środek. A najlepiej zrobić good enough, kod i tak się często zmienia.

0
bustard2 napisał(a):

Czy ten styl można podsumować dwoma słowami: "programowanie funkcyjne"?

Tylko jako bardzo, bardzo, bardzo duży skrót. Najlepiej tak nie mówić. Żeby coś należało do paradygmatu funkcyjnego to musi się nie dać móc redefiniować zmiennych i funkcje musiałyby nie mieć efektów ubczonych; a obie te rzeczy da się zrobić w Kotlinie.

0

@Charles_Ray: mógłbyś rozwinąć myśl dot. funkcyjnego? Zakładam, że chodzi o to, że niektóre z tych funkcji zostały tak zdefiniowane, jakby był wewnątrz jakiejś klasy, bo używają jakichś zewnętrznych somethingBuilderów? Dodatkowo, w kodzie występują metody i obiekty. Czy chodzi o coś jeszcze? Czy twoim zdaniem podany kod jest ok, czy zostały w nim nadużyte konstrukty?
@Riddle: idąc tym tokiem rozumowania, to w Kotlinie nie da się napisać kodu z użyciem paradygmatu funkcyjnego i JetBrains nas oszukuje deklarując, że ten język wspiera programowanie funkcyjne? Czy zatem Haskell "należy" do programowania funkcyjnego? Nie znam tego języka, ale zdaje się że obsługuje źródła danych typu pliki, bazy danych itd, bo inaczej byłby mało użyteczny.

0
bustard2 napisał(a):

@Riddle: idąc tym tokiem rozumowania, to w Kotlinie nie da się napisać kodu z użyciem paradygmatu funkcyjnego i JetBrains nas oszukuje deklarując, że ten język wspiera programowanie funkcyjne?

JetBrains (i każdy kto wydał jakiś język) się chwali że "wspierają" różne paradygmaty, bo chcą mieć wszechstronne języki - takie które można użyć "do wszystkiego".

Problem polega na tym że każdy paradygmat (nie ważne jaki, funkcyjny, obiektowy, proceduralny, strukturalny, imperatywny, deklaratywny) to nie jest żadna dodatkowa funkcja lub cecha. Paradygmat to jest zawsze pewne ograniczenie, a więc zabranie funkcji. W przypadku programowania strukturalnego nie pozwalamy używać goto, w przypadku funkcyjnego np. nie pozwalamy redefiniować zmiennych, w przypadku obiektowego nie pozwalamy odczytywać niezakenkapsulowanych wartości.

Paradygmat funkcyjny, to trochę tak jak "paradygmat wegentariański". Jak staniesz się wegetarianinem to nie dostajesz żadnych nowych "mocy", tylko po prostu zgadzasz się już nie jeść mięsa (tak samo jak programista który pisze funkcyjnie zgadza się - pomiędzy innymi - nigdy nie redefiniować zmiennych). Teraz jeśli ktoś mówi że jego język "wspiera programowanie funkcyjne" to tak jakby powiedzieć że Twój stek "wspiera posiłek wegetariański" - owszem, możesz dodać sałatkę do steka, ale to nie znaczy że faktycznie ten posiłek stanie się wegetariański - podobnie jak dodanie funkcyjnych elementów do języka nie znaczy że język stanie się funkcyjny.

Czy zatem Haskell "należy" do programowania funkcyjnego?

Tak.

Nie znam tego języka, ale zdaje się że obsługuje źródła danych typu pliki, bazy danych itd, bo inaczej byłby mało użyteczny.

No obsługuje, i co z tego?

@bustard2: Jak chcesz się dowiedzieć więcej to spróbuj obejrzeć np ten filmik: youtube.com/watch?v=7Zlp9rKHGD4.

1

w przypadku funkcyjnego np. nie pozwalamy redefiniować zmiennych,

Raczej nie pozwalamy modyfikować wartości zmiennych. Przesłanianie czyli np. zdefiniowanie zmiennej o tej samej nazwie, w tym samym zasięgu ale o innej wartości jest jak najbardziej dozwolone. Dozwolone jest też przepięcie tej samej nazwy na inną wartość (co ma miejsce we wszelkich parametrach funkcji).

Co do pytania zadanego w temacie, dla mnie to tylko inny sposób zapisu tego samego. Semantyka jest taka sama, więc nie widzę powodu aby jedno miało być mniej funkcyjne od drugiego. Jednak generalnie preferuję przypisywanie wartości do zmiennych i możliwe upraszczanie wyrażeń. Głównie dlatego, że taki kod dużo łatwiej się debuguje - debuggery zwykle automatycznie wyświetlają wartości zmiennych, natomiast przy bardzo długim, rozbudowanym wyrażeniu ciężko czasem dojść jakie były wartości podwyrażeń.

Nazwane zmienne stanowią dodatkową dokumentację, zwiększając czytelność kodu.

To jest generalnie podobny problem co "ile kodu umieszczać w jednej funkcji". Można przecież pisać długie tasiemcowe funkcje na setki linii i też będzie działać, ale jednak większość się raczej zgadza że takie coś jest zwykle mniej czytelne niż kiedy kod jest podzielony na mniejsze, dobrze nazwane kawałki.

4
bustard2 napisał(a):

Scope functions (let, also, apply, run itd.) są bardzo uniwersalne i można je stosować w aż tak wielu różnych miejscach, że nachodzi pokusa, żeby stosować je niemalże wszędzie gdzie się da, eliminując potrzebę deklarowania zmiennych w kodzie prawie do zera (poza parametrami funkcji, o ile to zaliczamy je do zmiennych) oraz stosując wyłącznie "single-expression functions". Czy w Kotlinie warto zawsze i wszędzie używać scope functions? A może warto ograniczyć ich użycie maksymalnie i stosować np. tylko typowe konstrukcje typu ?.let{ ... }? Zgaduję, że prawda leży gdzieś po środku. Jakie jest wasze zdanie na ten temat?

Problem jest raczej z nie używaniem scope functions niż z nadużywaniem. Te scope functions to takie podstawowe klocki w kotlinie, da się bez nich obejść, ale wychodzi z tego kod przeważnie gorszy.

Czy ich stosowanie zwiększa, czy zmniejsza przejrzystość kodu?

IMO wpływa lekko pozytywnie. Ale bez szału.

Czy ułatwia czy utrudnia rozwój?

Zależy co to jest rozwój. Np. na rozwój umiejętności używania goto, pętli, i multiple returnów wpływa raczej negatywnie.

Czy wpływa na niezawodność kodu?

Zdecydowanie - dajesz kompilatorowi więcej szans (typy muszą się zgadzać). Błedy związane z przypadkowo pominiętymi lub nadmiarowymi instrukcjami i ich rezultatami powinny prawie nie występować. Dzięki temu kod lepiej przeżywa refaktoring i mniej testowania wymaga.

W ramach ćwiczenia napisałem w takim stylu ok. 1k linijek kodu nie używając ani val, ani var, ani klasycznych funkcji z blokiem kodu, a jedynie "single-expression function". Mam kilka swoich przemyśleń na ten temat, m. in. jaką przewagę ma taki styl pisania, ale wolałbym się tym nie dzielić w pierwszym poście. Czy ten styl można podsumować dwoma słowami: "programowanie funkcyjne"?

Tak, to warunek konieczny programowania funkcyjnego - wszystko jest wyrażeniem => da się zapisać przy pomocy funkcji.
Jak jeszcze te funkcje będą czyste to już wystarczy. Tu np. also ładnie pokazuje gdzie są efekty uboczne.

Zrobiłem taki plugin do kotlinowego lintera, który wymusza między innymi te wyrażenia i wiele innych rzeczy
https://github.com/neeffect/kure-potlin - jak zastosujesz to w zasadzie robisz z kotlina język funkcyjny. Co w pewnych modułach ma dużo sensu, choć ze względu na frameworki / liby - zwykle nie ma sensu(nie uda się) w całym kotlinowym projekcie.

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