Poruszę dziś pojęcie z programowania, które sięga wyższego szczebla kodu źródłowego. Dowiesz się czym jest w programowaniu niejaka funkcja wyższego rzędu i czym się różni od funkcji standardowej wyjaśnionej tutaj. Jeżeli chcesz posiąść nową wiedzę z terminologii, to prosimy 🙂!

FUNKCJA WYŻSZEGO RZĘDU W PROGRAMOWANIU, TO WYŻSZY POZIOM FUNKCJONALNOŚCI

Są takie potrzeby projektowe, w których możesz sobie zażyczyć wstawiać pewnego rodzaju "wymienne instrukcje" do tej samej funkcji. Z różnych powodów - na przykład do tego samego komunikatu pragniesz wstawić inny tekst wiersz niżej. Albo pod wpływem jakiegoś zdarzenia chcesz odtwarzać raz taki dźwięk, a w innym przypadku inny. I wiele innych 💡.

Właśnie wtedy na 99% będziesz rozważać implementację funkcji wyższego rzędu 😇! Funkcja wyższego rzędu (ang. higher-order function) dotyczy funkcji, która spełnia jeden z poniższych warunków:

  • przyjmuje parametr formalny typu "inna funkcja",
  • zwraca wartość typu "wskaźnik do funkcji".

Już rozwijam skróty myślowe.

Jedną z charakterystycznych cech takiej funkcji jest przyjmowanie parametru jako innej funkcji o odpowiedniej sygnaturze. Oznacza to, że jedna funkcja będzie oczekiwać drugiej i "ta druga" będzie brać udział podczas wykonywania instrukcji "w tej pierwszej" (nie wiedziałem jak to inaczej napisać 😅). Takie podejście bardzo uelastycznia wywołania takich funkcji, ponieważ otwiera to drogę do wstawiania RÓŻNYCH instrukcji co wywołanie!

Jedynym "ale" będzie to, że "przydzielanie" funkcji (bądź wyrażenia lambda) będzie się ograniczać do takich, u których sygnatura zgadza się ze wzorcem np. funkcja typu "void" bez parametrów, funkcja zwracająca typ "float" bez parametrów, funkcja typu "void" akceptująca dwa parametry typu "double" i tak dalej.

Drugą opcją jest funkcja wyższego rzędu, która nie przyjmuje parametrów jako funkcji, natomiast zwraca wskaźnik do niej! Można tak zrobić, żeby funkcja przyjmowała jakieś dane (nie jako funkcje), lecz za to żeby zwracała wskaźnik do funkcji 👈.

Ciekawostka związana z nazwą. "Wyższy rząd" dotyczy poziomu czy "stopnia" abstrakcji. Poziom pierwszy dotyczy konkretnych danych np. liczb, łańcuchów znaków, referencji do obiektów i to zarówno w miejscu parametrów, jak i zwracanych wartości. Takie funkcje są określane "funkcjami pierwszego rzędu". Poziom drugi (czyli ten nazywany jako wyższy) przeznaczony jest dla funkcji, które przyjmują inną funkcję (jako parametr) bądź zwracają wskaźnik do niej (jako wartość wynikowa). Samo określenie "funkcji wyższego rzędu" pochodzi zaś od królowej nauk, w której także funkcje przyjmują inne funkcje lub je zwracają.

Ostatnia rzecz, zanim przejdziemy do praktyki: jest to fundamentalny element paradygmatu funkcyjnego! Jego istotą jest opieranie się na dostarczaniu danych wejściowych do funkcji i otrzymywaniu wyniku na wyjściu, tylko co ważne, bez jakichkolwiek modyfikacji samych danych pomiędzy 🧨.

Przykład kodu źródłowego

Przykład dotyczy zaprezentowania funkcji akceptującej parametr typu "funkcja". Będzie w języku C, jako że temat nie dotyczy tylko jednego języka programowania. Język C jak najbardziej posiada taką możliwość, lecz ostrzegam, że zapis może być dla Ciebie bardzo trudny do zrozumienia, szczególnie jeśli dopiero zaznajamiasz się z funkcjami wyższego rzędu!!! Poziom abstrakcji zapewniany przez zapisy być może znane Tobie z innych języków, "uatrakcyjniono" trochę później 🙂.

Dlatego też, żeby Ci pomóc to skumać, dorzuciłem przykłady w językach C++ i C#, które wykonują to samo. Celem jest pokazanie Ci czym się różnią między sobą składniowo ✔️. A przy okazji, poznasz tajemnicę działania w językach wyższego poziomu, niż C 😄!

Oto język C:

#include <stdio.h>

void doSomething(int integer, int (*function)(int));
int getSquareOf(int integer);

int main(void)
{
	doSomething(15, getSquareOf);

	return 0;
}

void doSomething(int integer, int (*function)(int))
{
	if(function != NULL)
	{
		printf("%d\n", function(integer));
	}
}

int getSquareOf(int integer)
{
	return integer*integer;
}

Efektem końcowym programu jest wypisanie liczby 225 w terminalu, czyli 15 do potęgi 2. Tłumaczę poszczególne fragmenty kodu.

Na samej górze (pod dyrektywami "include") mamy prototypy funkcji. Pierwsza uruchamia naszą funkcję wyższego rzędu, a druga jest funkcją "wkładaną" w miejsce parametru tej pierwszej. Dla doprecyzowania - to jest funkcja wyższego rzędu 👇:

void doSomething(int integer, int (*function)(int));

ponieważ za parametr przyjmuje wskaźnik do funkcji. To jest ten fragment:

int (*function)(int)

Przekładając na język polski: wskaźnik do funkcji zwracającej liczbę całkowitą ("int"), która akceptuje jeden parametr typu "int".

W funkcji "main" mamy wywołanie naszej funkcji wyższego rzędu ("doSomething"), w środku której wprowadzamy dwa parametry: liczbę 15 (która będzie potem przypisana w środek funkcji z parametru) i - teraz uwaga - samą nazwę funkcji ⚠️! Nie wstawiamy żadnych nawiasów okrągłych, bo to nie jest wywołanie!

Ten zapis stanowi kluczowy dowód na to, że nazwa funkcji jest wskaźnikiem do niej 💡! Żeby doszło do takiego manewru, musi się zgadzać zarówno typ zwracanej wartości, jak i układ parametrów formalnych tzn. te same typy i ta sama kolejność.

W samym ciele funkcji wyższego rzędu ("doSomething") mamy wplecione wywołanie naszej funkcji z parametru i zwróć uwagę, że dopiero w tym miejscu mamy wywołanie:

printf("%d\n", function(integer));

Instrukcja wykona się wtedy i tylko wtedy, gdy podano referencję do funkcji, stąd jest otoczona instrukcją warunkową, czy jest różne od "NULL".

"getSquareOf" jest najprostszą konstrukcją ze wszystkich, bo tu mamy standardowe zwrócenie kwadratu podanej liczby i tyle 😁.

Tak się kończy przykład w języku C. Teraz zobacz język C++:

#include <iostream>
#include <functional>

void doSomething(int integer, std::function<int(int)> function);

int main()
{
	std::function<int(int)> getSquareOf = [](int integer)
	{
		return integer*integer;
	};

	doSomething(15, getSquareOf);

	return 0;
}

void doSomething(int integer, std::function<int(int)> function)
{
	if(function)
	{
		std::cout << function(integer) << std::endl;
	}
}

W prototypie funkcji wyższego rzędu, mamy zmianę parametru funkcyjnego na inną postać:

std::function<int(int)> function

Korzystam tutaj z dobrodziejstwa STL jakim jest "std::function". Wymaga dyrektywy u góry:

#include <functional>

Opiszę tylko nawiasy kątowe, w których zapis oznacza miejsce na funkcję przyjmującą jedną liczbę całkowitą ("int" na zewnątrz) i zwracającą liczbę całkowitą ("int" w środku nawiasów).

Funkcja "getSquareOf" powędrowała do środka funkcji "main" jako wyrażenie lambda, przez co zyskujemy na mniejszej złożoności kodu 👍. A reszta taka sama, tylko "printf" zamieniłem na odpowiednik C++ do wypisywania treści na terminal ("cout") i skróciłem wyrażenie instrukcji warunkowej.

A teraz C#:

public class Program
{
	public static void Main(string[] args)
	{
		Func<int, int> getSquareOf = integer => integer*integer;

		DoSomething(15, getSquareOf);
	}

	private static void DoSomething(int integer, Func<int, int> function)
	{
		Console.WriteLine(function?.Invoke(integer));
	}
}

Od razu lepiej, co 😄? Sekret właśnie został dla niektórych ujawniony! W C#, takie budulce jak "Action" i "Func", wykorzystują "pod maską" wskaźniki do funkcji! Rozgałęzienia porozrzucane na zewnątrz miejsca użycia są elegancko "zapakowane" w słowa kluczowe:

<int, int>

i wyrażenia lambda:

integer => integer*integer;

Reszta jest taka sama, z wyjątkiem innego zapisu wywołania naszej funkcji w parametrze:

function?.Invoke(integer)

Korzystam z operatora zabezpieczającego przed wywołaniem na referencji równej "null" (?) i metody "Invoke". Można napisać klasycznie:

function(integer);

jednak chcąc dodać znak zapytania, musimy to zrobić "naookoło".

Funkcja wyższego rzędu w programowaniu

Funkcja wyższego rzędu to funkcja akceptująca parametr w postaci wskaźnika do funkcji bądź zwraca taki wskaźnik na wyjściu. Jako element programowania funkcyjnego, potrafi uczynić funkcję bardzo elastyczną przez możliwość wstawiania instrukcji "wymiennych".


Funkcja wyższego rzędu została wyjaśniona najlepiej, jak byłem w stanie 😅. Musiałem wyjątkowo wstawić przykłady również innych języków, niż C, wierząc że to pomoże Ci zrozumieć jak to zostało skonstruowane. Jak to opanujesz, to na pewno zechcesz dużo razy umieszczać w projektach funkcje z instrukcjami "wymiennymi" 💪.

PODOBNE ARTYKUŁY