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 , unit
któ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 + 1
matematycznej 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ęć.