Scala programowanie funkcyjne – wyczyść swoje funkcje i nie kłam. Za parę lat podziękujesz sam sobie.

"Płoną na stosach heretycy, Ci co kłamią w deklaracjach i komentarzach.

A smród jest wielki, a krzyki nieopisane"

- Luźny Interpretator -

Programowanie imperatywne musi odejść.

Nie ma miejsca na świecie dla programistów, którzy nie znają podejścia funkcyjnego. Jak pisać kod, to tylko funkcyjnie. Podejście obiektowe jest jak dinozaur. Programowanie funkcyjne to konieczność.

Może lekko podkręciłem powyższe wypowiedzi (ale wierz mi, lekko i nie wszystkie). Nie zmienia to faktu iż coraz częściej spotykam się z tego typu retoryką na forach, blogach czy w mediach społecznościowych.

Wiesz co? Chrzań to. Naprawdę. To kolejna wojna w stylu co jest lepsze Atari czy Commodore. Windows czy Linux. Dieta beztłuszczowa czy oparta o same tłuszcze.

More...

Co ja o tym myślę? 

Zarabiało się najlepiej, pisząc gry jednocześnie i na Atari i na Commodore. Obsługa dźwięku i obrazu inna, ale procesor ten sam, więc 80% kodu była identyczna na oba komputerki.

Używam Windowsa jako desktopa, a Linuksa na wszystkich serwerach, które obsługuję (a sporo ich mam pod opieką).

Dieta? Zacznij się żywić zdrowo w odpowiednich oknach czasowych  i zapomnij o dietach (no, chyba że musisz ze względów zdrowotnych). 

Programowanie funkcyjne czy imperatywne? Używaj obu podejść tam, gdzie Ci to wygodne, gdzie Ci pasuje i gdzie czujesz, że jest lepszym rozwiązaniem. 

Nie polaryzuj się. Myśl głową, a nie nagłówkami w Twoich ulubionych mediach. Na koniec dnia, zostaniesz sam ze swoją wiedzą, swoimi poglądami z robotą do zrobienia. NIKT nie wie lepiej od Ciebie jak sobie z nią poradzić.

Programowanie funkcyjne jest trudne?

Jest jeszcze jednego bardzo spolaryzowane podejście, którego nie lubię. Programowanie funkcyjne jest trudne, nie da się tego nauczyć bez magistra z matematyki.

Ile razy gdy postanowiłeś, że spróbujesz? Jednak już przy pierwszej próbie w głowie pojawiał Ci się wielki, krzyczący czerwienią neon ZA T UDNE!!! 

R już nie świeci, padła żarówka.

Co masz z nim zrobić? Zapomnij. Podeptaj. Schowaj do tylnej, dziurawej kieszeni Twoich ulubionych, przetartych jeansów, na której kiedyś czerwonym flamastrem  (w chwili upojenia… radością życia oczywiście) wysmarowałeś czerwonym mazakiem "Wypadło? Niech leży. Podnosisz na własne ryzyko".

Programowanie funkcyjne, to umiejętność, kolejny element Twojego prywatnego arsenału, warto ją mieć, warto jej używać, ale nie buduj dla niej kapliczek.

Jest to podejście, ani trudne, ani łatwe, po prostu inne od tego czego używałeś dotychczas i dlatego być może się go nawet boisz (taki nasz mały wewnętrzny strach, przed opuszczaniem stref komfortu, czegoś co umiemy i świetnie znamy i rozpoczynaniem wędrówki w nieznane).

Scala jest idealnym rozwiązaniem dla ludzi, którzy szukają świetnego narzędzia łączącego oba światy. Programowania imperatywnego i funkcyjnego.

Jest najlepszym miejscem, w którym można wykorzystywać umiejętności tworzenia aplikacji obiektowych i łączyć je z początku nieśmiało ze światem funkcyjnym.

Czysta funkcja (pure function)

To funkcja która 

1.

Nie ma side effect (wysłanie czegoś na ekran, do bazy, zmiana zmiennej globalnej itp)

2.

Jest zgodna z referential transparency (zobacz niżej)

3.

Zawsze dla określonych danych wejściowych mamy określony rezultat (taka jest idea)

Co nam to daje

1.

Proste testowanie (jeśli do wykonania nie używa żadnych danych poza tymi przekazanymi w argumentach, nie zależy od żadnego zewnętrznego stanu, to jej testowanie jest trywialne)

2.

Bezpieczeństwo z punktu widzenia wielowątkowości (j.w. funkcja niezależna od zewnętrznych stanów programu, czyli może być wywoływana równolegle niezliczoną ilość razy bez konieczności blokowania)

Najprostsza czysta funkcja:

def pure(x: Int): Int = x

Jak ją najłatwiej pobrudzić?

def dirty(x: Int): Int = {
  println(x)
  x
}

Hej, ale czy to znaczy, że nie mogę vara użyć w funkcji? Pewnie, że możesz. tak długo jak robisz to wewnątrz swojej funkcji i zmieniasz niczego na zewnątrz, to możesz robić co Ci się żywnie podoba.

Co to jest to referential transparency?

W wielkim skrócie.

Jeśli masz funkcję np. o nazwie funcX() i przypiszesz rezultat tej funkcji np. do stałej stalaX, to bez względu na to, czy dalej w kodzie użyjesz wywołania funcX czy stalaX, rezultat musi być taki sam.

Jeśli zawsze jest, to znaczy, że Twoja funkcja spełnia warunki referential transparency.

Prosty przykład:

def funcX(i: Int): Int = i * 2

def funcY(i: Int): Int = {
  val stalaX = funcX(i)
  stalaX
}

assert(funcX(100) == funcY(100))

Obiecuję, że wrócimy i rozwiniemy ten temat w przyszłości, na dzisiaj to proste wytłumaczenie jest w zupełności wystarczające. 

Total function

Funkcja jest uznana za totalną, kiedy jest zdefiniowana dla wszystkich możliwych danych wejściowych.


Poniżej przykład funkcji czystej i totalnej jednocześnie.

def not(b: Boolean): Boolean = ! b

Metoda, która nie jest funkcją czystą (zmienia wartość zmiennej globalnej counter) oraz nie jest funkcją totalną (brak definicji dla dzielenia przez 0)

def div(i: Int): Int = {
  counter += 1
  10 / i
}

I na koniec mamy funkcję totalną, ale nie czystą (ponownie ze względu na zmiany w zmiennej globalnej).

def total(i: Int): Int = {
  counter += 1
  i
}

Przestań kłamać

Czy funkcję div można przerobić, tak, żeby była zarówno totalna, jak i czysta? Można, wystarczy przestać kłamać.

Ejj, ale jak kłamać? O czym ty mówisz? To proste, bardzo przepraszam, ale wrednie Cię okłamałem. Popatrz dokładnie. Co Ci mówi ta deklaracja?

def div(i: Int): Int

Mówi, że dla każdego włożonego Inta, dostaniesz Inta z powrotem, prawda?

Jest to deklaracja, która niemal krzyczy, że jest czysta i pachnąca. Jaka jest rzeczywistość, wiemy oboje. Wystarczy, że prześlesz 0 i zaczyna się robić niemiło. Co gorsza, nie dość, że nie dostaniesz w odpowiedzi Inta, to pojawi Ci się w kodzie niezapowiedziany Exception. 

Krótko mówiąc, ktoś nas tutaj okłamał. Obiecywał zawsze Inta i nie dotrzymał słowa.

Co, gdyby zadeklarować ją inaczej?

def div(i: Int): Try[Int]

Jak wygląda teraz całość?

import scala.util.Try
import scala.util.Success
import scala.util.Failure

def div(i: Int): Try[Int] = { 
  Try(100 / i) 
}

Jak tego użyć?

def test(i: Int) = {
  div(i) match {
    case Success(r) =>
      println(r)
			
    case Failure(ex) =>
      println(s"Problem podczas wykonywania operacji: ${ex.getMessage}")    
  }
}

test(10)
test(0)  

Widzisz? Wystarczy przestać kłamać, a kod od razu staje się przyjemniejszy i samodokumentujący się.

Prawda

def div(i: Int): Try[Int]

Kłamstwo

def div(i: Int): Int

Funkcja jako first-class-citizen

Co to w sumie znaczy?

1.

Funkcja może być przypisana do stałej/zmiennej


Najpierw przypiszemy funkcję do stałej.

val a = (x: Int, y: Int) => x + y

Możemy też przypisać prostą funkcję add do stałej a.


Następnie sprawdzamy, czy dla tych samych argumentów wywołania rezultaty są równe.

def add(x: Int, y: Int): Int = x + y
val b = add _
assert(b(2,2) == add(2,2))

Można też przypisać tę samą funkcję do stałej przy użyciu _ (wildcard operator).

val c: (Int, Int) => Int = _ + _
assert(b(3,3) == c(3,3))
2.

Funkcja może być przesłana jako argument wykonania innej funkcji.


Dla przykładu prosta metoda wykonująca funkcję przesłaną w argumencie func na dwóch przesłanych liczbach

def calculate(x: Int, y: Int, func: (Int, Int) => Int): Unit = {
  val text = try {
    func(x, y)
  } catch {
    case e: Throwable => s"Wystąpił błąd podczas wykonywania operacji: ${e.getMessage}"
  }
  println(text)
}
  
val sum = (i: Int, y: Int) => (i + y)
val multi =  (i: Int, y: Int) => (i * y)
val div =  (i: Int, y: Int) => (i / y)

calculate(10, 20, sum)
calculate(2, 5, multi)
calculate(2, 0, div)
3.

Funkcja jako rezultat może zwrócić… inną funkcję


Funkcja formatString w zależności od argumentu upper - zwraca funkcję, zamieniającą znaki w stringu na małe, bądź duże litery.

def formatString(upper: Boolean) = 
  if (upper) 
    (s: String) => s.toUpperCase 
  else 
    (s: String) => s.toLowerCase

def doFormat(s: String, upper: Boolean) = {
  val format = formatString(upper)
	format(s)  
}

println(doFormat("Ala ma kota", upper = true))
println(doFormat("I TO JUŻ KONIEC...", upper = false))

Nie prawda to wcale nie koniec, funkcję formatString można również użyć bezpośrednio.

println(formatString(true)("Można też tak"))

Przy takim zapisie, w pierwszym nawiasie mamy argument wywołania funkcji formatString, natomiast w drugim nawiasie jest argument, jaki przekazujemy do funkcji otrzymanej w rezultacie.


Dzięki temu możemy ją od razu wykonać, bez konieczności posiłkowania się zmiennymi pomocniczymi.

Immutability


Programowanie funkcyjne to przepływ strumienia informacji, jego transformacja, ale bez modyfikowania niczego dookoła. Dlatego programowanie funkcyjne tak świetnie sprawdza się w tworzeniu aplikacji wielowątkowych. 


Prosty przykład.


Masz za zadanie wyciągnąć z otrzymanej w argumencie listy intów wszystkie elementy nieparzyste, podnieść je do kwadratu, a następnie otrzymane kwadraty zsumować ze sobą i rezultat wyświetlić na ekranie.


Ile już widzisz pętli przed oczami?


Ja nie widzę żadnej.

def example1(list: List[Int]):Unit = {
  def sum = 
    list
        .filter(x => x % 2 == 1) // [1]
        .map(x => x * x) // [2]
        .reduce((x,y) => x+y) // [3]

  println(sum)
}

Widzę tylko strumień danych i transformacje.


Najpierw [1] filtruje przesłaną w argumencie listę, w rezultacie otrzymując kolejną listę złożoną wyłącznie z nieparzystych elementów.


Następnie [2] przy pomocy mapowania podnoszę każdy element do kwadratu, tworząc w rezultacie kolejną listę, na samym końcu [3] redukuję ją do pojedynczego inta, sumując wszystkie elementy.


To samo można zapisać nieco inaczej używając notacji wildcardowej.

def example2(list: List[Int]):Unit = {
  def sum = 
    list
        .filter(_ % 2 == 1)
        .map(x => x * x)
        .reduce(_ + _)
  println(sum)
}

Jednak korzystając z informacji zawartych w tym artykule, wiesz już na pewno, że istnieje jeszcze jedna forma zapisu, którą możesz użyć:

def example3(list: List[Int]):Unit = {  
  val odd: (Int) => Boolean = _ % 2 == 1
  val sum: (Int, Int) => Int = _ + _  
  val exp = (i: Int) => i * i
  
  def execute = 
      list
        .filter(odd)
        .map(exp)
        .reduce(sum)
  println(execute)
}

Programowanie funkcyjne - podsumowanie

Staraj się, gdy możesz używać czystych funkcji (nie zawsze jest to możliwe, ale jeśli jest, warto o nich pamiętać).

Nie kłam na etapie deklaracji i pomyśl czasami o danych jak o transformowanym strumieniu.

Szybko zaobserwujesz, że tworzenie testów stanie się prostsze, a zarządzanie wielowątkowością nie będzie sprawiało takich problemów jak wcześniej. 

Jeśli do tego wszystkiego dodasz jeszcze możliwości, jakie daje Akka, życie stanie się prostsze i bardziej kolorowe 🙂

To wszystko w dzisiejszym wpisie. 

Daj znać, o czym masz ochotę poczytać następnym razem.

Chcesz Nauczyć Się Scali?

Jestem tutaj, by Ci pomóc.