Scala - czego unikać, dobre rady na początek

1

Zacząłem naukę Scali, głównie z myślą o nauce Sparka. Wiem, że mógłbym użyć do tego Javy, ale skoro i tak nie ma w tym kraju pracy dla ludzi z moim wykształceniem :P to stwierdziłem, że utrudnię sobie zadanie i wykorzystam ten czas do nauki czegoś nowego. Urzekł też mnie na swój sposób Vavr więc zrobię krok dalej przynajmniej jeżeli chodzi o podstawy i zobaczymy jak pójdzie. Zdaję sobie również sprawę, że w każdym języku można pisać kod działający, ale niekoniecznie sensowny biorąc pod uwagę specyfikę użytego języka.

Dlatego zwracam się z prośbą o wskazówki.

Na co zwrócić szczególną uwagę?
Co powinno się robić w tym języku, a czego absolutnie nie?
Jakich naleciałości z Javy unikać?

Póki co mam takie przemyślenia.

  1. Immutability uber alles! Swoją drogą czy nie uważacie, że val powinno być wartością defaultową dla pól?
  2. Rekurencja, rekurencja ogonowa, akumulatory. Z tym chyba mam póki co największy problem. Ciężko przestawić się na takie myślenie. Mam nadzieję, że to kwestia treningu.
  3. Unikać stosowania pętli. W kursie na Udemy pada wręcz stwierdzenie, że jeżeli chcesz użyć pętli while to znaczy, że robisz to źle. Używaj rekurencji.
  4. Pisać bez efektów ubocznych. Jak to rozumieć? Ja to interpretuje w ten sposób, że za każdym razem jeżeli dokonujemy jakiejś operacji na obiekcie to należy zwrócić jego nową instancję lub zmapować na inny typ.

Na ten moment na tyle.

2

Chyba dobrze sprawy ująleś, raczej staraj się używać definicji funkcji ze znakiem równości (zamiast tych z klamrą), ciesz się parametrami klas, zapomniałeś wspomnieć o używaniu funkcji jako parametrów i funkcji anonimowych. Skoro rekurencja to dla ciebie coś nowego, to nie zapomnij o funkcjach map, fold, filter, itp. Chociaż też bez popadania w przesadę, czasem można sobie pozwolić na odrobinę imperatywnego programowania, zwłaszcza jeśli używasz javowego API, które nie pozostawia wyboru — po prostu opakowujesz to i na wyższym poziomie już robisz funkcyjnie. Jak zrozumiesz po co się pisze kod funkcyjny, myslę, że zrozumiesz. :)
Co do skutków ubocznych, to głównie chodzi o to, żeby wynik działania funkcji zależał tylko i wyłącznie od parametrów, w szczególności nie używać zmiennych globalnych. Jednak i tutaj, bez przesady, jak musisz skontaktować się z bazą danych, zrobić jakieś I/O to nie ma raczej opcji. Najważniejsze, żeby możliwie te kawałki odseparować od reszty.

1

@elwis:

jak musisz skontaktować się z bazą danych, zrobić jakieś I/O to nie ma raczej opcji.

Jak to nie ma opcji? Po to mamy monady IO i insze, żeby robić czyste funkcje, które zależą tylko od argumentów, nawet jeśli trzeba pogadać z brudnym światem zewnętrznym.
Ale jak ktoś się uczy scali to na początku można to olać.a

0

Co rozumieć przez unsafeRun? Dosłownie wykonanie zapytania do bazy czy też dajmy na to odczyt z pliku, które zwrócą nam None bądź też rzucą wyjątkiem? Czyli jako parametr przekazuje funkcję najniższego poziomu do głównej metody, a potem wynik operacji przetwarzam jak vavrowego eithera mapami i itp.?

Co do skutków ubocznych, to głównie chodzi o to, żeby wynik działania funkcji zależał tylko i wyłącznie od parametrów, w szczególności nie używać zmiennych globalnych.

A co ze zmianą stanu obiektu? Z jednej strony mogę sobie wyobrazić, że do każdego mapowania będziemy mieli nową instancję czy też nawet klasę, która będzie rozszerzała podstawowy obiekt, ale z drugiej może to oznaczać, że nam ilość prawie identycznych klas zacznie szybko puchnąć. Jednak jeżeli mielibyśmy się kurczowo trzymać tej zasady to nie powinno się używać setterów co samo z siebie wyniknie jeżeli ograniczymy się tylko do korzystania val. Tylko co w sytuacji jeżeli w momencie tworzenia obiektu nie mamy wszystkich danych i jakieś pole zostawimy niezainicjalizowane? Optymalnie czy też raczej poprawnie będzie wówczas mieć dwie klasy: początkową i rozszerzoną?

1

Nie wiem czy to odpowie na pytanie co to znaczy unsfafeRun, bo przykład trochę zakłada, że to już wiemy, ale przykład jak to działa.

https://zio.dev/docs/getting_started.html

unsafeRun - to taka funkcja, która wykonuje program czysty (zapisany w postaci monady IO) na rzeczywistym świecie, gdzie są pliki, brud, awarie itd.
taka "profanacja".

A co ze zmianą stanu obiektu?

Nie ma takich rzeczy :-) (*)
Co do puchnięcia klas itd. - w pewnym sensie to prawda, programując funkcyjnie wykorzystuje sie tak do 10x więcej typów niż w analogicznym kodzie imperatywnym (obiektowym). Z drugiej strony - dzięki
type inference, tuples (krotki), first class function i kilku innym ficzurom jezyka - wcale tego nie widać - kod czasem przypomina dynamiczny jezyk w stylu js.

*
W praktyce - zmianę stanu w sposób czysty można zrobić korzystając z tzw. monady stanu. Przy czym monada stanu jest zupełnie czysta, nie ma tam żadnych brzydkich trików.
Można też odwołać się do czegoś w stylu Ref, które jest wewnętrznie nieczyste - taki odpowiednik wskaźnika w stylu funkcyjnym. Obiektu co prawda nie mutujemy, ale mamy wskaźnik na aktualną jego wersję (i możemy podmienić). To taka brudna alternatywa dla monady stanu. Używa się we frameworkach ze względu na wydajność i wygodę.

0
olfeusz napisał(a):

Tylko co w sytuacji jeżeli w momencie tworzenia obiektu nie mamy wszystkich danych i jakieś pole zostawimy niezainicjalizowane? Optymalnie czy też raczej poprawnie będzie wówczas mieć dwie klasy: początkową i rozszerzoną?

Zależy od przypadku.

Możesz zamodelować takie pola jako Option i domyślnie mieć tam None:

case class Person(firstName: String, maybeSecondName: Option[String])
val person = Person("abc", None)
...
val person2 = person.copy(maybeSecondName = Some("cdf")

W innych sytuacjach sensowniejsze może być zamodelowanie tego za pomocą kilku case class z transformacjami między nimi. Czego się nie wybierze to raczej nigdy nie następuje eksplozja prawie identycznych klas - zawsze się znajdzie na to jakiś sensowny sposób ;)

1

Wygląda na to że trafiłeś na 2 w 1: Kurs Scali + Podstawy programowania funkcyjnego.

Rekurencja, rekurencja ogonowa, akumulatory. Z tym chyba mam póki co największy problem. Ciężko przestawić się na takie myślenie. Mam nadzieję, że to kwestia treningu.

W praktyce (a piszę to jako programista Scali 2 lata expa) poza fanatykami nikt tak nie pisze. Użycie foldów i rekrurencji to prosta droga do pisania przekombinowanego i nieczytelnego kodu. Jeżeli chcesz się nauczyć postaw FP to faktycznie warto takie ćwiczenia porobić żeby zrozumieć pewne koncepcje (tak jak dużo robi się dziedziczenia i hierarchii podczas nauki OOP). W produkcyjnym kodzie takie akademickie konstrukcje nie są już tak mile widziane...

Unikać stosowania pętli. W kursie na Udemy pada wręcz stwierdzenie, że jeżeli chcesz użyć pętli while to znaczy, że robisz to źle. Używaj rekurencji.

W kontekście Scali oznacza to że bardzo dużo rzeczy można zrobić za pomocą api strumieni (map/filter/take/drop/takeFirst/toList itp.) i pętle nie pojawiają się już tak często jak w java 8. Po prostu api strumieni jest o wiele bardziej zaawansowane. W kontekście twojego kursu: więcej wciskania FP ;)

Moje rady:

  • Używaj case class do modelowania danych.
  • Pisz krótkie czytelne metody. Twórz dużo klas.
  • Unikaj definiowania operatorów typu ++ czy :| - to nie jest czytelne.
  • Odpuść bardziej zaawansowane rzeczy jak HKT czy makra.
  • W skali jedną rzecz można napisać na 10 sposobów np. println { 1 }, znajdź dobry linter i używaj go od początku pozwoli Ci to wyłapać nieoczywiste błędy.
  • Użycie Java'owych bibliotek często ssie, lepiej znaleźć Scalowe nakładki lub odpowiedniki niż bujać się z konwersją Int <-> java.lang.Integer itp.

Z mojego blogu (ilustracja problemów z językiem):
http://blog.marcinchwedczuk.pl/scala-nesting-monads
http://blog.marcinchwedczuk.pl/passing-functions-as-arguments-in-scala-what-can-go-wrong
http://blog.marcinchwedczuk.pl/scala-wtf-1
http://blog.marcinchwedczuk.pl/pitfalls-of-using-Mockito-with-Scala

PS. Skala to niestety bardzo skomplikowany język i wymaga czasu zanim zostanie porządnie przyswojona. Tutaj dla przykładu kolejny wpis o pattern matchingu: http://blog.marcinchwedczuk.pl/ultimate-guide-to-scalas-match-expression który dobrze obrazuje złożoność języka...

0
olfeusz napisał(a):
  1. Rekurencja, rekurencja ogonowa, akumulatory. Z tym chyba mam póki co największy problem. Ciężko przestawić się na takie myślenie. Mam nadzieję, że to kwestia treningu.

serio? A co z wydajnością? Przecież scala to jvm. I co z czytelnością?

1

@NamingException: Rekurencja ogonowa w kwestii wydajności to trochę co innego niż zwykła rekurencja, ma możliwe optymalizacje: https://www.aptsoftware.com/scala-tail-recursion-optimisation-and-comparison-to-java/

3

Tylko bezkształtne interpretery na wolnych monadach przy akompaniamencie kotów tagless final i dobrze skalibrowanej optyki.

Edit:

3
olfeusz napisał(a):

Zacząłem naukę Scali, głównie z myślą o nauce Sparka.

Nie chce nikogo rozczarowywać, ani tym bardziej obrażać, ale po przeczytaniu dwóch książek o Sparku zorientowałem się, że ludzie używający Sparka to nie są programiści tylko ludzie umiejący programować. Dokładniej ich nazwa zawodu to DataCośtam (nie pamiętam dokładnie, bo dużo się tych zawodów DataCośtam porobiło)

Co to oznacza? Jako nieprogramiści po prostu programują nie zastanawiając się nad Clean Code i innymi rzeczami. To że ich kod przypadkowo wygląda bardziej funkcyjnie niż kod w Javie 6 wynika z tego że MapReduce i podobnym algorytmom do przetwarzania danych po drodze z programowaniem funkcyjnym. Może w większych projektach są jakieś zasady, które się przestrzega bardziej niż w przykładach książkowych, ale na konferencjach słyszałem narzekania na specjalistów DataCośtam że dostarczają kod usiany zmiennymi aa i się tym nie przejmują. IHMO Prawdopodobieństwo znalezienia tam kogoś do rozmowy o wyższości monady ZIO nad State z Cats jest dość niskie.

Póki co mam takie przemyślenia.

Twoje przemyślenia są dobre, ale w Scali może być trudno ich przestrzegać, bo Scala jest funkcyjno imperatywna i daje wybór jak chcesz programować. A pokusa żeby zrobić imperatywny skrót jest duża.
Jak byś próbował pisać w Haskellu to wyboru byś nie miał. Tzn w Haskellu można napisać, kod który wygląda jak imperatywny, ale jest to dużo trudniejsze niż napisanie kodu w duchu Haskella czyli czysto funkcyjnie.

Unikać stosowania pętli. W kursie na Udemy pada wręcz stwierdzenie, że jeżeli chcesz użyć pętli while to znaczy, że robisz to źle. Używaj rekurencji.

Po zamianie pętli for na map/flatMap/fold tych miejsc na rekurencję wcale nie ma tak dużo. Zostają pętle while/do-while, ale to czasem też da się zamienić na funkcje biblioteczne. Np jeśli chcemy liczby Fibonacciego większe od 100, a mniejsze od 1000 to można liczby Fibonacciego zamodelować jako nieskończony leniwy strumień, a potem pobrać za pomocą funkcji bibliotecznych dropWhile i takeWhile, dzięki czemu nie mamy ani jawnej pętli, ani jawnej rekurencji.

BTW w Scali często warto użyć pętli for bo tak naprawdę nie jest to stricte pętla tylko mini język do przetwarzania monad czyli odpowiednik do-notation z Haskella. Oczywiście zarówno do-notation jak i pętli for w Scali nie należy nadużywać, bo istnieją funkcje biblioteczne, które robią to w czytelniejszy sposób. (Tylko że potem zostajemy z funkcjami typu liftM2, ale to już mój problem)

0

To trochę jak byś wymagał od lekarza by miał dwie specjalizacje. Lub więcej. Zdarza się, ale rzadko. Osoby odpowiedzialne za stricte obrabianie danych mogą mieć inny zestaw umiejętności niż programista i dla nich może się liczyć przede wszystkim końcowy wynik, a to w jaki sposób go osiągną może być dla nich kwestią drugorzędną.

bo Scala jest funkcyjno imperatywna

Rzucam być może herezją, ale taka mnie naszła myśl, Czy czasami ta cała funkcyjność Scali to nie jest ściema w postaci przykrycia zaprojektowanej pod język obiektowy JVM nową składnią i możliwościami innego niż javowy kompilatora? No bo jeżeli zamiast anonimowej implementacji apply zastąpię to lambdą to przecież tak czy siak, co prawda nie wprost i to jest właśnie ta całość funkcyjność, wołam new Function().

1
olfeusz napisał(a):

To trochę jak byś wymagał od lekarza by miał dwie specjalizacje. Lub więcej. Zdarza się, ale rzadko. Osoby odpowiedzialne za stricte obrabianie danych mogą mieć inny zestaw umiejętności niż programista i dla nich może się liczyć przede wszystkim końcowy wynik, a to w jaki sposób go osiągną może być dla nich kwestią drugorzędną.

Trochę schodzimy z tematu, ale dwóch na dwóch lekarzy w rodzinie mojej żony ma po dwie specjalizacje. Najpierw zrobili internę, ale opatrywanie ludzi z pourywanymi kończynami im się znudziło.
BTW Dwóch specjalizacji bardzo często wymagają od programistów nazywa się to fullstack lub developer + SysOps :(
BTW2 nie czepiam się ludzi od obrabiania danych, bardziej jestem zawiedziony że nie znajdę tam tego czego szukam, a szukam miejsce gdzie produkcyjnie używają programowania czystofunkcyjnego i monad :)

bo Scala jest funkcyjno imperatywna

Rzucam być może herezją, ale taka mnie naszła myśl, Czy czasami ta cała funkcyjność Scali to nie jest ściema w postaci przykrycia zaprojektowanej pod język obiektowy JVM nową składnią i możliwościami innego niż javowy kompilatora? No bo jeżeli zamiast anonimowej implementacji apply zastąpię to lambdą to przecież tak czy siak, co prawda nie wprost i to jest właśnie ta całość funkcyjność, wołam new Function().

Czy to jest ściema? Ogólnie JVM jest imperatywny, kompilator LLVM też ma imperatywny bitecode, komputery też są imperatywne. Komputera w pełni funkcyjnego chyba jeszcze nie zbudowano. Były kiedyś lisp-maszyny (dedykowane komputery do wykonywania Lispa), ale Lisp nie jest w 100% funkcyjny. Tak więc zawsze kompilowany język funkcyjny kończy jako imperatywny kod.
Tutaj jeszcze moje dwie myśli:

  • Obiektowość w Scali nie jest problemem (chociaż polimorfizm w OOP jest ułomny, lepsze są TypeClassy). Problemem jest imperatywny, zmienny stan.
  • Twórcy EtaLang (kompilatora Haskella na JVM) udowodnili że można tego JVMa przykryć taką ilością abstrakcji że nawet czystofunkcyjny kod się wykona.

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