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”.
Spis treści
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.
Zdjęcie główne artykułu pochodzi z stocksnap.io.