Blog

Golang dla hobbystów. Funkcje - odsłona druga

Niniejszy wpis zamyka temat funkcji. Omawiam w nim zagadnienie funkcji jako wartości pierwszklasowych oraz funkcji anonimowych.

Golang dla hobbystów. Funkcje - odsłona druga


Dzisiejszy tekst będzie poświęcony bardziej “zaawansowanym” zagadnieniom związanych z funkcjami w GO, co zresztą zostało wstępnie zasygnalizowane w zakończeniu poprzedniego wpisu. Oczywiście całość tematu nie zostanie tym samym wyczerpana, tym niemniej dzisiaj na tapet biorę dwa aspekty funkcji w GO, czyli zagadnienie funkcji jako typów pierwszoklasowych (zdecydowana większość wpisu poświęcona zostanie tej kwestii) oraz funkcji anonimowych.

Funkcje jako typy pierwszoklasowe

Język Go obsługuje funkcje pierwszoklasowe, czyli funkcje w Go są traktowane jak „obiekty pierwszej klasy”. Niewiele to jednakże wyjaśnia, więc mówiąc bardziej po ludzku: w GO funkcje można przypisywać do zmiennych, a następnie wywoływać je z użyciem tych właśnie zmiennych. Albo jeszcze inaczej rzecz stawiając: w Go funkcje podobnie jak inne wartości są specyficznym typem danych, a tym samym mogą być przypisywane do zmiennych oraz dopuszcza się przekazywanie ich jako argumentów do innych funkcji. Skoro mogą one spełniać rolę argumentów innych funkcji, to również możliwe jest by były one zwracane jako wynik działania tej czy innej funkcji. Tym samym mogą występować we wszystkich tych miejscach oraz w rolach, w których mogą się pojawić liczby całkowite, łańcuchy znaków czy inne typy znane z GO. Podejrzewam, że najprościej będzie to zrozumieć na przykładzie:

package main

import "fmt"

func suma(x int, y int) int {
	return x + y
}

func main() {
	Wynik := suma
	fmt.Println(Wynik(2, 2))
}

W powyższym listingu do zmiennej “Wynik” została przypisana bezpośrednio funkcja o nazwie “suma”, a nie tylko wynika działania tej ostatniej. Od tego momentu można używać tej nowo powołanej do życia zmiennej niemal tak samo jak funkcji do niej przypisanej. Faktu, że mamy tutaj do czynienia z opisaną sytuacją dość łatwo zidentyfikować po tym, iż po prawej stronie naszej krótkiej deklaracji zmiennej podana została nazwa funkcji, ale bez jednoczesnego użycia nawiasów oraz wymaganych argumentów pomiędzy nimi. Gdyby te nawiasy się pojawiły zaraz po nazwie funkcji (oraz umieszczony zostały odpowiednie argumenty pomiędzy nimi) wówczas do zmiennej “Wynik” przypisany były wynik tej funkcji (pod warunkiem, że funkcja ta zwraca cokolwiek w efekcie swojego działania), czyli w tym wypadku liczbę całkowitą będąca efektem dodawania przekazanych argumentów.

By uczynić to jeszcze czytelniejszym wykonanym to samo ćwiczenie, ale przy użyciu pełnej deklaracji, czyli takiej gdzie najpierw zadeklarujemy zmienną “wynik” wskazując jej typ i dopiero w następnym kroku przypiszemy do niej funkcję “suma”. Będzie to również dobry punkt wyjścia do omówienia pewnego aspektu związanego z zagadnieniem funkcji jako wartości pierwszej klasy.

package main

import "fmt"

func suma(x int, y int) int {
	return x + y
}

func main() {
	var Wynik func(int, int) int
	Wynik = suma
	fmt.Println(Wynik(2, 2))
	fmt.Printf("Nasza właśnie utworzona zmienna Wynik jest typu: %T \n", Wynik)
}

Ponieważ w tym drugim wypadku określony został typ naszej zmiennej, widać wyraźnie, że nie interesuje nas sam wynik dodawania dwóch liczb, ale cały mechanizm, który za takim rachowaniem stoi. Co więcej w takiej sytuacji zadeklarowane parametry oraz zwracana wartość funkcji są częścią typu naszej zmiennej, w efekcie tego nasza zmienna "Wynik" może przechowywać dowolną funkcję pod jednym warunkiem: liczba oraz typ parametrów jak i lista wyników funkcji (ewentualnie jej brak) będzie się zgadzać z tym co pierwotnie zadeklarowaliśmy przy tworzeniu tej zmiennej.

Specjalnie w ostatnim wierszu ciała funkcji main dodałem instrukcję, która dzięki funkcji fmt.Printf nakazane zostało kompilatorowi zwrócenie komunikatu z informację o typie naszej zmiennej. W rezultacie w terminalu powinniśmy zobaczyć następujący tekst: “Nasza właśnie utworzona zmienna Wynik jest typu: func(int, int) int”.

Oznacza to tyle mniej więcej, iż każda próba przypisania do zmiennej “Wynik” innej wartości niż funkcja, która na wejściu oczekuje dwóch liczb całkowitych, zaś na końcu zwraca wynik w postaci liczby całkowitej, zakończy się odmową współpracy ze strony kompilatora GO ze względu na brak zgodności typów.

Idźmy dalej. Skoro funkcja jest takim samym “obiektem” jak każda inna wartość, to można ją równie dobrze przekazywać jak parametr lub oczekiwać jej jako wyniku działania w przypadku innej funkcji. Tym drugim przypadkiem nie będę się zajmował, natomiast by zobrazować jak to w praktyce może wyglądać, posłużę się prostym przykładem.

Załóżmy, że chcemy mieć funkcję, którą nazwiemy “prostaAlgebra”. Jej zadaniem będzie dokonywać prostych obliczeń na dwóch liczbach całkowitych i w efekcie zwracanie wyniku tych operacji. Na potrzeby ćwiczenia przyjmijmy, że istnieją tylko dwa podstawowe działania matematyczne w tym zakresie, czyli dodawanie i odejmowanie. Tym samym na początek utworzymy dwie funkcje odpowiadające za rachowanie w zakresie sumy i różnicy. Następnie powołamy do życia wspomnianą funkcję o nazwie “prostaAlgebra”, która będzie mogła przyjąć te dwie pierwsze funkcje jako jeden z argumentów, zaś na wyjściu będzie zwracała wynik operacji dodawania lub odejmowania. Dorzucimy w gratisie jeszcze jedną funkcję, która pozoruje tylko wykonywanie jakiś obliczeń, ponieważ zawsze zwraca ten sam wynik (liczbę 44), ale spełnia wymagania funkcji “prostaAlgebra” w zakresie zgodności parametrów i wyników. Całość wówczas wyglądałaby następująco:

package main

import "fmt"

func dodawanie(x int, y int) int {
	return x + y
}

func odejmowanie(x int, y int) int {
	return x - y
}

func zwracamLiczbę(x, y int) int {
	return 44
}

func prostaAlgebra(x, y int, f func(int, int) int) int {
	return f(x, y)

}

func main() {
	fmt.Println(prostaAlgebra(5, 2, dodawanie))
	fmt.Println(prostaAlgebra(5, 2, odejmowanie))
	fmt.Println(prostaAlgebra(5, 2, zwracamLiczbę))
}

Nie jest to zapewne najmądrzejszy przykład, który można wymyślić, by zilustrować omawiane zagadnienie, ale wydaje mi się, że daje jakiś ogląd tego zagadnienia.

Funkcje anonimowe

Na koniec będzie jeszcze bardzo krótko o funkcjach anonimowych. Poniekąd zagadnienie to było już sygnalizowane w poprzednim wpisie, ale dla przypomnienia: funkcje “nazwane”, czyli takie jakie było dotąd omawiane, mogą być deklarowane wyłącznie na poziomie pakietu. W efekcie nie ma możliwości - gdyby z jakiegoś powodu była taka potrzeba - tworzenie funkcji o bardziej lokalnym charakterze, co jednak do końca jest prawdą, bo tutaj w sukurs przychodzą właśnie funkcje anonimowe.

Fachowo nazywa się je również literałami funkcji i tworzy się je podobnie jak “normalne” funkcje. To co je tak naprawdę różni to fakt, że nie zawierają one żadnej nazwy po słowie kluczowym func. Trzymając się naszej algebraicznej narracji niżej przedstawiony został sposób wykorzystania tego konstruktu w obrębie funkcji main.

package main

import "fmt"

func main() {

	suma := func(x int, y int) int {
		return x + y
	}
	różnica := func(x int, y int) int {
		return x - y
	}

	fmt.Println(suma(5, 2))
	fmt.Println(różnica(5, 2))
}

Na “Do widzenia”

To by było wszystko, co na początek z grubsza warto wiedzieć na temat funkcji w GO. Dlatego w kolejnym wpisie przejdziemy do tematu jakoś pokrewnego z funkcjami, czyli omówione zostaną metody. Opanowanie tych ostatnich będzie dobrym punktem wyjścia do wprowadzenie zagadnienia struktur, czyli ostatniej już kwestii, którą zamknąć zamierzam ten cykl wpisów. A tymczasem pozostaje mi podziękować i do zobaczenia.