Wprowadzenie do pojęć związanych z programowaniem funkcjonalnym w języku F#

Programowanie funkcjonalne to styl programowania, który podkreśla wykorzystanie funkcji i niezmiennych danych. Programowanie funkcjonalne typizowane polega na połączeniu programowania funkcjonalnego z typami statycznymi, takimi jak F#. Ogólnie rzecz biorąc, w programowaniu funkcjonalnym kładzie się nacisk na następujące koncepcje:

  • Funkcje jako podstawowe konstrukcje, których używasz
  • Wyrażenia zamiast instrukcji
  • Niezmienne wartości dla zmiennych
  • Programowanie deklaratywne w przypadku programowania imperatywnego

W tej serii zapoznasz się z pojęciami i wzorcami w programowaniu funkcjonalnym przy użyciu języka F#. Po drodze dowiesz się też trochę języka F#.

Terminologia

Programowanie funkcjonalne, podobnie jak inne paradygmaty programowania, zawiera słownictwo, które w końcu trzeba będzie nauczyć. Poniżej przedstawiono niektóre typowe terminy, które będą widoczne przez cały czas:

  • Funkcja — funkcja jest konstrukcją, która będzie generować dane wyjściowe w przypadku danych wejściowych. Bardziej formalnie mapuje element z jednego zestawu na inny zestaw. Ten formalizm jest podnoszony do konkretnego pod wieloma względami, zwłaszcza w przypadku korzystania z funkcji działających na kolekcjach danych. Jest to najbardziej podstawowa (i ważna) koncepcja programowania funkcjonalnego.
  • Wyrażenie — wyrażenie jest konstrukcją w kodzie, która generuje wartość. W języku F# ta wartość musi być powiązana lub jawnie ignorowana. Wyrażenie może być trywialnie zastąpione przez wywołanie funkcji.
  • Czystość - Czystość jest właściwością funkcji, tak aby jej wartość zwracana zawsze stała dla tych samych argumentów, i że jej ocena nie ma skutków ubocznych. Czysta funkcja zależy całkowicie od jego argumentów.
  • Przezroczystość referentialną — przezroczystość referentialną jest właściwością wyrażeń, dzięki czemu można je zastąpić danymi wyjściowymi bez wpływu na zachowanie programu.
  • Niezmienność — niezmienność oznacza, że nie można zmienić wartości w miejscu. Jest to sprzeczne ze zmiennymi, które mogą ulec zmianie.

Przykłady

W poniższych przykładach przedstawiono te podstawowe pojęcia.

Funkcje

Najbardziej typową i podstawową konstrukcją w programowaniu funkcjonalnym jest funkcja . Oto prosta funkcja, która dodaje 1 do liczby całkowitej:

let addOne x = x + 1

Jego podpis typu jest następujący:

val addOne: x:int -> int

Podpis można odczytać jako "addOne akceptuje int nazwę x i utworzy ".int Bardziej formalnie mapowanie addOne wartości z zestawu liczb całkowitych na zestaw liczb całkowitych na zestaw liczb całkowitych. Token -> oznacza to mapowanie. W języku F# zwykle można przyjrzeć się podpisowi funkcji, aby uzyskać poczucie tego, co robi.

Dlaczego więc podpis jest ważny? W programowaniu funkcjonalnym wpisanym implementacja funkcji jest często mniej ważna niż rzeczywista sygnatura typu! Fakt, że addOne dodaje wartość 1 do liczby całkowitej, jest interesujący w czasie wykonywania, ale podczas tworzenia programu, fakt, że akceptuje i zwraca wartość, int jest to, co informuje, jak rzeczywiście użyjesz tej funkcji. Ponadto po poprawnym użyciu tej funkcji (w odniesieniu do podpisu typu) diagnozowanie wszelkich problemów można wykonać tylko w treści addOne funkcji. Jest to impuls do programowania funkcjonalnego wpisanego.

Wyrażenia

Wyrażenia to konstrukcje, które oceniają wartość. W przeciwieństwie do instrukcji, które wykonują akcję, wyrażenia można traktować jako wykonywanie akcji, która zwraca wartość. Wyrażenia są prawie zawsze używane w programowaniu funkcjonalnym zamiast instrukcji.

Rozważmy poprzednią funkcję . addOne Treść addOne elementu jest wyrażeniem:

// 'x + 1' is an expression!
let addOne x = x + 1

Jest to wynik tego wyrażenia, który definiuje typ addOne wyniku funkcji. Na przykład wyrażenie tworzące tę funkcję można zmienić tak, aby było innym typem string, takim jak :

let addOne x = x.ToString() + "1"

Podpis funkcji to teraz:

val addOne: x:'a -> string

Ponieważ dowolny typ w języku F# może ToString() być wywoływany, typ x został wykonany jako ogólny (nazywany automatycznym uogólnianiem), a wynikowy typ to string.

Wyrażenia nie są tylko ciałami funkcji. Możesz mieć wyrażenia, które generują wartość używaną w innym miejscu. Typową z nich jest :if

// Checks if 'x' is odd by using the mod operator
let isOdd x = x % 2 <> 0

let addOneIfOdd input =
    let result =
        if isOdd input then
            input + 1
        else
            input

    result

Wyrażenie if generuje wartość o nazwie result. Należy pamiętać, że można całkowicie pominąć result wyrażenie, tworząc if wyrażenie treści addOneIfOdd funkcji. Kluczową rzeczą do zapamiętania na temat wyrażeń jest to, że generują wartość.

Istnieje specjalny typ , unitktóry jest używany, gdy nie ma nic do zwrócenia. Rozważmy na przykład tę prostą funkcję:

let printString (str: string) =
    printfn $"String is: {str}"

Podpis wygląda następująco:

val printString: str:string -> unit

Typ unit wskazuje, że nie jest zwracana rzeczywista wartość. Jest to przydatne, gdy masz rutynę, która musi "wykonywać pracę", mimo że nie ma wartości, aby zwrócić w wyniku tej pracy.

Jest to w ostrym przeciwieństwie do programowania imperatywnego, gdzie równoważna if konstrukcja jest instrukcją, a tworzenie wartości jest często wykonywane przy użyciu zmiennychmutujących. Na przykład w języku C#kod może być napisany w następujący sposób:

bool IsOdd(int x) => x % 2 != 0;

int AddOneIfOdd(int input)
{
    var result = input;

    if (IsOdd(input))
    {
        result = input + 1;
    }

    return result;
}

Warto zauważyć, że język C# i inne języki w stylu C obsługują wyrażenieternarne, które umożliwia programowanie warunkowe oparte na wyrażeniach.

W programowaniu funkcjonalnym rzadko są mutować wartości z instrukcjami. Chociaż niektóre języki funkcjonalne obsługują instrukcje i mutacje, często nie należy używać tych pojęć w programowaniu funkcjonalnym.

Czyste funkcje

Jak wspomniano wcześniej, czyste funkcje to funkcje, które:

  • Zawsze oceniaj tę samą wartość dla tych samych danych wejściowych.
  • Nie mają skutków ubocznych.

Warto myśleć o funkcjach matematycznych w tym kontekście. W matematyce funkcje zależą tylko od ich argumentów i nie mają żadnych skutków ubocznych. W funkcji f(x) = x + 1matematycznej wartość parametru f(x) zależy tylko od wartości x. Czyste funkcje w programowaniu funkcjonalnym są takie same.

Podczas pisania czystej funkcji funkcja musi zależeć tylko od jego argumentów i nie wykonać żadnej akcji, która powoduje efekt uboczny.

Oto przykład funkcji nieczystej, ponieważ zależy od globalnego, modyfikowalnego stanu:

let mutable value = 1

let addOneToValue x = x + value

Funkcja addOneToValue jest wyraźnie nieczysła, ponieważ value może zostać zmieniona w dowolnym momencie, aby mieć inną wartość niż 1. Ten wzorzec w zależności od wartości globalnej należy unikać w programowaniu funkcjonalnym.

Oto kolejny przykład funkcji nieczystej, ponieważ wykonuje efekt uboczny:

let addOneToValue x =
    printfn $"x is %d{x}"
    x + 1

Mimo że ta funkcja nie zależy od wartości globalnej, zapisuje wartość x w danych wyjściowych programu. Chociaż nie ma z natury nic złego w tym celu, oznacza to, że funkcja nie jest czysta. Jeśli inna część programu zależy od czegoś zewnętrznego z programem, takiego jak bufor wyjściowy, wywołanie tej funkcji może mieć wpływ na inną część programu.

Usunięcie instrukcji sprawia, printfn że funkcja jest czysta:

let addOneToValue x = x + 1

Mimo że ta funkcja nie jest z natury lepsza niż poprzednia wersja z printfn instrukcją , gwarantuje, że ta funkcja zwraca wartość. Wywołanie tej funkcji dowolną liczbę razy generuje ten sam wynik: po prostu generuje wartość. Przewidywalność podana przez czystość jest czymś, do czego dąży wielu programistów funkcjonalnych.

Niezmienność

Na koniec jedną z najbardziej podstawowych koncepcji typowego programowania funkcjonalnego jest niezmienność. W języku F# wszystkie wartości są domyślnie niezmienne. Oznacza to, że nie można ich modyfikować w miejscu, chyba że jawnie oznaczysz je jako modyfikowalne.

W praktyce praca z niezmiennymi wartościami oznacza, że zmieniasz podejście do programowania z "Muszę coś zmienić", na "Muszę utworzyć nową wartość".

Na przykład dodanie wartości 1 do wartości oznacza utworzenie nowej wartości, a nie zmutowanie istniejącej wartości:

let value = 1
let secondValue = value + 1

W języku F# następujący kod nie mutuje value funkcji; zamiast tego wykonuje sprawdzanie równości:

let value = 1
value = value + 1 // Produces a 'bool' value!

Niektóre języki programowania funkcjonalnego w ogóle nie obsługują mutacji. W języku F# jest obsługiwany, ale nie jest to domyślne zachowanie wartości.

Ta koncepcja rozszerza się jeszcze bardziej na struktury danych. W programowaniu funkcjonalnym niezmienne struktury danych, takie jak zestawy (i wiele innych) mają inną implementację, niż początkowo można oczekiwać. Koncepcyjnie coś takiego jak dodanie elementu do zestawu nie powoduje zmiany zestawu, powoduje utworzenie nowego zestawu z wartością dodaną. W ramach tych działań często jest to realizowane przez inną strukturę danych, która umożliwia efektywne śledzenie wartości, dzięki czemu można uzyskać odpowiednią reprezentację danych w wyniku.

Ten styl pracy z wartościami i strukturami danych ma kluczowe znaczenie, ponieważ wymusza traktowanie każdej operacji modyfikujące coś tak, jakby tworzy nową wersję tej rzeczy. Pozwala to na spójność w programach takich rzeczy jak równość i porównywalność.

Następne kroki

W następnej sekcji szczegółowo omówiono funkcje, eksplorując różne sposoby ich używania w programowaniu funkcjonalnym.

Używanie funkcji w języku F# umożliwia głębokie eksplorowanie funkcji, pokazując, jak można ich używać w różnych kontekstach.

Dalsze informacje

Seria Thinking Functionally to kolejny świetny zasób, aby dowiedzieć się więcej o programowaniu funkcjonalnym w języku F#. Obejmuje podstawy programowania funkcjonalnego w pragmatyczny i łatwy do odczytania sposób, używając funkcji języka F# do zilustrowania pojęć.