Backend

Dlaczego powinieneś częściej korzystać z programowania funkcyjnego w Ruby

Ruby to język wieloparadygmatowy. Większości kojarzy się z programowaniem obiektowym i jest w tym dużo racji, bo to dominująca konwencja, ponieważ Ruby znany jest właśnie z tego, że wszystko jest obiektem (nawet cyfry czy klasy są obiektami!). Sporo programistów często nieświadomie korzysta jednak z programowania funkcyjnego w Ruby. W tym artykule pokażę jakie możliwości w tym względzie daje nam ten język oraz będę Cię zachęcał do częstszego stosowania tego podejścia.


Krzysztof Kempiński. Programuje od przeszło 10 lat w Ruby on Rails i od niedawna w Elixir. Zajmował się też takimi technologiami jak iOS czy PHP. Obecnie pracuje jako team leader w berlińskim startupie. Rozwija podcast skierowany do branży IT o nazwie “Porozmawiajmy o IT”.


Czym charakteryzuje się programowanie funkcyjne?

Programowanie funkcyjne to jedna z filozofii programowania. Paradygmat funkcyjny to model, wzorzec myślenia o programie. W tym przypadku programujemy wykorzystując funkcje, czyli fragment kodu, który przyjmuje argumenty i zwraca wynik. Istotne jest zaznaczenie, że nie ma tutaj tzw. side effects. Przykładowo jeśli funkcja przyjmuje argument będący listą, to nie modyfikuje w żaden sposób źródła tego argumentu. Działa na jego kopii.

W programowaniu funkcyjnym funkcje możemy traktować jako wartości. Możemy przekazywać je jako parametry lub tworzyć z nich programy poprzez odpowiednie zestawienie ich w ciąg. Można to porównać do linii produkcyjnej. Funkcje to kolejne maszyny otrzymujące dane na wejściu i generujące wynik będący wejściem do kolejnej funkcji.

Niektóre funkcje mogą przyjmować inne funkcje jako argument lub takowe zwracać. Mówimy wtedy o higher-order functions a język, który umożliwia takie operacje określamy jako first-class function support.

Kolejną ważną koncepcją jest niemutowalność stanu. Oznacza to, że raz zdefiniowana zmienna co do swojej wartości nie ulega zmianie. Jeśli chcemy ją jakoś zmodyfikować, konieczne jest utworzenie kopii.

Elementy programowania funkcyjnego w Ruby

W rzeczywistości w Ruby nie ma klasycznych funkcji z definicji programowania funkcyjnego. Wszystko w Ruby jest obiektem. Istnieją jednak pewne elementy tego języka, które przypominają funkcje. Są to:

Lambda

Definiujemy je jak funkcje:

add = lambda { |x, y| x + y}

lub w skrócie

add = -> (x,y) { x + y }

Istotną ich cechą jest to, że możemy je przekazywać jako argumenty do innych metod:

do_math(add, 2, 2)

Żeby wykonać kod zawarty w lambdzie należy zastosować na niej metodę #call(params):

add.call(2, 2)

Bloki

Są to małe, anonimowe funkcje, które mogą być przekazane do metod jako parametry. Przykładowo metoda each na liście przyjmuje blok, który będzie uruchomiony na każdym elemencie tej listy:

[1, 2, 3].each {|elem| puts elem }

Proc

Jest to konstrukt podobny do lambda. Różni się sposobem tworzenia, traktowaniem wymagalności parametrów i sposobem zwracania wyników (return wewnątrz proc zwraca z obecnej metody):

t = Proc.new {|x, y| “Hi!”}
t.call

Higher-order functions

W Ruby możemy budować metody, które przyjmują funkcje:

words = ["foo", "bar"]
words.map do |word|
"Hi " + word
end

lub je zwracają

def sum_function(a, b)
lambda {|a, b|}
end

sum_fun = sum_function(1, 2)
sum_fun.call

Bardzo wiele tego typu metod zawartych jest w module Enumerable (any?, map, each, select, ...).

Zalety programowania funkcyjnego w Ruby

Wprowadzając pewne założenia programowania funkcyjnego do kodu pisanego na co dzień w Ruby jesteśmy w stanie poprawić nie tylko jego czytelność, ale i jakość wynikową. Trzeba jednak zaznaczyć, że musi być to wynikiem trzymania się konwencji, gdyż Ruby nie wymusza na programistach stosowania żadnych z poniżej opisanych zasad.

Największą korzyść można osiągnąć poprzez stosowanie się do zasady niemutowalności stanu i braku side-effects. Pozwala to unikać niespodziewanych sytuacji typu:

def all_different_from_first?(arr)
first = arr.shift
arr.all? { |n| n != first }
end
arr = [1, 2, 3, 4]
all_different_from_first?(arr)

Spodziewalibyśmy się, że arr to ciągle [1, 2, 3, 4]. Tymczasem przez zastosowaniu arr.shift, arr w tym miejscu to [2, 3, 4]. Lepsza byłaby taka wersja:

def all_different_from_first?(arr)
arr[1..-1].all? { |n| n != arr.first }
end

Ograniczenie mutowalności można osiągnąć poprzez zaprzestanie stosowania attr_accessor i przejścia na attr_reader. Trzeba również zwrócić uwagę, w przypadku strings, arrays i hashes na następujące metody:

  • te kończące się ! (przykładowo gsub!)
  • delete
  • clear
  • pop / push / shift

Lepszym podejściem byłoby zrobienie klona (poprzez metodę #dup) i działanie na nowej wersji.

Kolejną dobrą praktyką ze świata programowania funkcyjnego jest stosowanie tzw. czystych funkcji (ang. pure functions), czyli takich, które nie mają żadnych side effects i które zawsze przy tym samym wejściu dają to samo wyjście, czyli przykładowo nie korzystają z żadnych zmiennych środowiskowych. Brak żadnych efektów ubocznych oznacza brak komunikacji z bazą danych, zmiany stanu innych obiektów czy nawet odczytu godziny systemowej. Takie funkcje jako lambda czy proc można łatwo testować i przekazywać jako argumenty do innych metod. Dodatkowo możemy je łączyć w łańcuchy realizujące większe zadania. Kod staje się bardziej czytelny i reużywalny. Ponadto takie funkcje są najczęściej niezależne od domeny, co poprawia separację części architektonicznych finalnego programu.

Podsumowanie

W artykule tym pokazałem czym charakteryzuje się programowanie funkcyjne i jakie benefity może to przynieść. Wskazałem również jakie elementy tego podejścia są dostępne w Ruby, a jakie możemy zacząć sami stosować wdrażając podstawowe koncepcje programowania funkcyjnego. Starałem się też wyjaśnić dlaczego w pewnych zastosowaniach szersze używanie tego paradygmatu przynosi korzyści.


baner

Zdjęcie główne artykułu pochodzi z stocksnap.io.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/dlaczego-powinienes-czesciej-korzystac-z-programowania-funkcyjnego-w-ruby/" order_type="social" width="100%" count_of_comments="8" ]