Predykaty, Funkcje i Programowanie Świadome

Wstęp

Programowanie, choć często kojarzone z czysto techniczną umiejętnością, w rzeczywistości może być głębszym, bardziej filozoficznym procesem. Jednym z ciekawszych aspektów, które wprowadza dyskusję na temat jakości kodu i jego struktury, są predykaty oraz czystość funkcji. W tym poście rozważymy, jak można podejść do funkcji z perspektywy logiki i odpowiedzialności, nie ograniczając się do prostych technikaliów, ale biorąc pod uwagę całościowy kontekst pisania kodu.

Porównanie funkcji, procesów i procedur

Funkcja "czysta" - Nie ma efektów ubocznych. Wynik zależy tylko od parametrów. Można ją traktować jako narzędzie obliczeniowe, nie zmieniające wyniku po otrzymaniu tego samego parametru kolejny raz przykładem może być funkcja kwadratowa.

Funkcja "nieczysta" - Ma efekty uboczne (np. zapis do pliku, odczyt z zewnętrznego API). Wynik może zależeć od czynników zewnętrznych (np. czas, stan systemu).

Proces - Jest to dynamiczny ciąg zdarzeń lub działań. Wymaga interakcji z zależnościami lub zmienia je. Może być niedeterministyczny, zależny od wielu zewnętrznych czynników.

Procedura - Sekwencja działań do wykonania, która może, ale nie musi, zwracać wynik. Z reguły nieczysta — jej celem jest zmiana stanu programu, bazy danych, systemu itd. Jest uporządkowana, działa według określonego schematu.

Rozumiejąc już rónicę między funkcją, procesem a procedurą możemy przejść dalej.

Czym jest predykat?

Predykat w informatyce to funkcja, która odpowiada na pytanie logiczne – zwraca wartość true lub false. W systemach programistycznych predykaty pełnią ważną rolę w kontrolowaniu logiki przepływu programu, na przykład w instrukcjach warunkowych czy pętlach.

Na przykład, prosta i znana już funkcja:

function isEven($n) {
    return $n % 2 == 0;
}

zwraca wartość logiczną true, jeśli liczba jest parzysta, a false w przeciwnym wypadku. Funkcje takie są czystymi predykatami, gdyż ich zadaniem jest jedynie odpowiedzieć na konkretne pytanie.

Predykaty a Funkcje Operacyjne

Jednakże, co w sytuacji, gdy funkcja wykonuje pewną operację (np. zapis do pliku lub bazy danych) i również zwraca wartość logiczną? Czy możemy ją nadal nazywać predykatem?

W praktyce często spotykamy się z funkcjami, które wykonują akcję, a nie tylko zwracają wartość logiczną. Na przykład funkcja:

function writeToFile($data) {
    // Zapis do pliku
    $success = file_put_contents('plik.txt', $data);
    return $success !== false;
}

Taka funkcja zwraca true lub false, w zależności od tego, czy operacja zakończyła się powodzeniem. Z formalnego punktu widzenia, nie jest to czysty predykat, ponieważ jej główną rolą nie jest odpowiedź na pytanie, lecz wykonanie operacji.

Czystość Funkcji i Programowanie Oparte na Procesach

Zanim przejdziemy do odpowiedzi na pytanie, jaką wartość powinna zwracać funkcja wykonująca operację, warto zrozumieć pojęcie czystości funkcji. Funkcja jest czysta, jeśli zawsze zwraca te same wyniki dla tych samych argumentów i nie ma żadnych efektów ubocznych (np. nie zapisuje do pliku ani nie odwołuje się do zmiennych globalnych).

Funkcje nieczyste wprowadzają zewnętrzne zależności – np. wynik operacji może zależeć od stanu pliku, do którego zapisujemy dane, albo od tego, czy serwer bazy danych działa poprawnie. Tego typu funkcje mogą być traktowane jako procedury lub procesy, ponieważ nie ograniczają się do zwracania wartości, ale operują na zewnętrznych zasobach.

W naszej dyskusji pojawił się wniosek, że procedury wykonują uporządkowany proces, który często prowadzi do zwrócenia pewnego rezultatu. Predykaty natomiast działają bardziej jako narzędzia do odpowiedzi na pytania logiczne.

Bool vs Rezultat – Co powinna zwracać funkcja?

Z perspektywy programisty, który chce pisać lepszy, bardziej świadomy kod, warto zadać sobie pytanie: Czy funkcja, która wykonuje operację (np. zapis do pliku), powinna zwracać wartość logiczną (true/false), czy może bardziej złożony obiekt reprezentujący wynik operacji?

Zwracanie wartości logicznej

Zwracanie true lub false ma swoje zalety – jest proste i bezpośrednie. Jednak w bardziej złożonych systemach może być niewystarczające, gdyż nie dostarcza pełnej informacji na temat tego, co dokładnie poszło nie tak. Takie podejście może prowadzić do kodu, który jest zbyt „naiwny” – jak dziecko, które odpowiada na pytania tak/nie, bez świadomości konsekwencji. Takie zachowanie utrudnia nam diagnostykę błędów i prawidłowe podjęcie dalszych kroków:

  • Co, jeśli zapis się nie udał, bo baza danych jest niedostępna?
  • Co, jeśli dany rekord już istnieje?
  • Co, jeśli dane były niepoprawne?
  • Zwracanie tylko wartości logicznej nie dostarczy szczegółowych informacji o tym, dlaczego operacja się nie powiodła.

    Zwracanie bardziej rozbudowanego rezultatu

    Lepszym podejściem, szczególnie w bardziej skomplikowanych systemach, może być zwracanie obiektu stanu operacji. Na przykład:

    class OperationResult {
        function __construct(private $success, private $message = null) {
            $this->success = $success;
            $this->message = $message;
        }
    
        function isSuccess() {
            return $this->success;
        }
    
        function message() {
            return $this->message;
        }
    }
    
    function writeToFile($data) {
        // Zapis do pliku
        $success = file_put_contents('plik.txt', $data);
        if ($success !== false) {
            return new OperationResult(true);
        }
        
        return new OperationResult(false, "Nie udało się zapisać do pliku");
    }

    Taki obiekt może mieć dodatkowe metody i atrybuty, pozwalając na bardziej szczegółową analizę stanu operacji. To podejście, choć wprowadza pewien narzut w postaci dodatkowego kodu, jest znacznie bardziej elastyczne i profesjonalne.

    Zarządzanie wyjątkami

    Inną ważną kwestią jest zarządzanie wyjątkami (exceptions). Jeśli operacja nie powiedzie się z powodu wyjątkowych okoliczności (np. błąd połączenia z bazą), można rozważyć zwracanie wyjątków.

    Zalety - Selektywna obsługa błędów: Mechanizm wyjątków pozwala na wyłapywanie specyficznych błędów tam, gdzie są one istotne, co prowadzi do bardziej modularnego i czytelnego kodu.

    Separation of concerns: Obsługa błędów przez wyjątki pozwala odseparować logikę operacji od logiki zarządzania błędami. Funkcja skupia się na wykonaniu zadania, a zarządzanie wyjątkowymi sytuacjami odbywa się poza nią.

    Wady - Możliwe nadużycie wyjątków: Wyjątki powinny być używane w rzeczywiście wyjątkowych przypadkach, takich jak nieoczekiwane błędy. Nadużywanie wyjątków do kontrolowania przepływu programu może prowadzić do trudniejszego do zrozumienia kodu.

    Programowanie Świadome

    Podsumowując, w naszej dyskusji doszliśmy do wniosku, że programowanie świadome polega na:

    1. Zrozumieniu głębszych konsekwencji każdej decyzji technicznej – zarówno dla systemu, jak i jego użytkowników.
    2. Przyjęciu odpowiedzialności za kod, który nie tylko działa, ale jest również łatwy do utrzymania, czytelny i rozszerzalny.
    3. Rozróżnianiu funkcji na te, które odpowiadają na pytania logiczne (predykaty), oraz te, które wykonują operacje (procedury lub procesy).
    4. Zwracaniu bardziej rozbudowanych wyników z funkcji operacyjnych, co zwiększa elastyczność i ułatwia debugowanie oraz obsługę błędów.

    Programista świadomy to nie tylko technik, ale także inżynier i filozof, który rozumie, że jego decyzje wpływają na cały ekosystem oprogramowania.