Przezroczystość referencyjna, odnosząca się do funkcji, wskazuje, że wynik zastosowania tej funkcji można określić tylko na podstawie wartości jej argumentów. Możesz pisać referencyjnie przezroczyste funkcje w dowolnym języku programowania, np. Python, Scheme, Pascal, C.
Z drugiej strony, w większości języków możesz także pisać funkcje nie referencyjnie przejrzyste. Na przykład ta funkcja Pythona:
counter = 0
def foo(x):
global counter
counter += 1
return x + counter
nie jest referencyjnie przejrzysty, w rzeczywistości dzwoni
foo(x) + foo(x)
i
2 * foo(x)
wygeneruje różne wartości dla każdego argumentu x
. Powodem tego jest to, że funkcja używa i modyfikuje zmienną globalną, dlatego wynik każdego wywołania zależy od tego zmieniającego się stanu, a nie tylko od argumentu funkcji.
Haskell, język czysto funkcjonalny, ściśle oddziela ocenę wyrażeń, w której stosowane są funkcje czyste i która zawsze jest referencyjnie przejrzysta, od wykonania akcji (przetwarzanie wartości specjalnych), która nie jest referencyjnie przejrzysta, tj. Wykonanie tej samej akcji może mieć za każdym razem inny wynik.
Tak więc dla dowolnej funkcji Haskell
f :: Int -> Int
i dowolną liczbą całkowitą x
, zawsze tak jest
2 * (f x) == (f x) + (f x)
Przykładem akcji jest wynik funkcji biblioteki getLine
:
getLine :: IO String
W wyniku oceny wyrażenia ta funkcja (właściwie stała) przede wszystkim generuje czystą wartość typu IO String
. Wartości tego typu są wartościami jak każda inna: możesz je przekazywać, umieszczać w strukturach danych, komponować je za pomocą specjalnych funkcji i tak dalej. Na przykład możesz zrobić listę takich akcji:
[getLine, getLine] :: [IO String]
Działania są wyjątkowe, ponieważ można powiedzieć środowisku wykonawczemu Haskell, aby je wykonał, pisząc:
main = <some action>
W takim przypadku, po uruchomieniu programu Haskell, środowisko wykonawcze przechodzi przez akcję związaną z nią main
i wykonuje ją, prawdopodobnie powodując efekty uboczne. Dlatego wykonywanie akcji nie jest względnie przejrzyste, ponieważ dwukrotne wykonanie tej samej akcji może dać różne wyniki w zależności od tego, co środowisko wykonawcze otrzymuje jako dane wejściowe.
Dzięki systemowi typów Haskell nigdy nie można użyć akcji w kontekście, w którym oczekiwany jest inny typ, i odwrotnie. Jeśli więc chcesz znaleźć długość łańcucha, możesz użyć length
funkcji:
length "Hello"
zwróci 5. Ale jeśli chcesz znaleźć długość ciągu odczytywanego z terminala, nie możesz pisać
length (getLine)
ponieważ pojawia się błąd typu: length
oczekuje wprowadzenia listy typów (a String jest rzeczywiście listą), ale getLine
jest wartością typu IO String
(akcja). W ten sposób system typów zapewnia, że wartość akcji podobna getLine
(której wykonanie odbywa się poza językiem podstawowym i która może być niereferencyjnie przezroczysta) nie może być ukryta wewnątrz wartości typu non-action Int
.
EDYTOWAĆ
Aby odpowiedzieć na pytanie exizt, oto mały program Haskell, który odczytuje wiersz z konsoli i drukuje jego długość.
main :: IO () -- The main program is an action of type IO ()
main = do
line <- getLine
putStrLn (show (length line))
Główna akcja składa się z dwóch podgrup, które są wykonywane sekwencyjnie:
getline
typu IO String
,
- drugi konstruuje się, oceniając funkcję
putStrLn
typu String -> IO ()
na podstawie jej argumentu.
Dokładniej, druga akcja jest zbudowana przez
- powiązanie
line
z wartością odczytaną przez pierwszą akcję,
- ocena funkcji czystych
length
(oblicz długość jako liczba całkowita), a następnie show
(zamień liczbę całkowitą na ciąg znaków),
- budowanie akcji poprzez zastosowanie funkcji
putStrLn
do wyniku show
.
W tym momencie można wykonać drugą akcję. Jeśli wpiszesz „Cześć”, wyświetli się „5”.
Zauważ, że jeśli otrzymujesz wartość z akcji za pomocą <-
notacji, możesz użyć tej wartości tylko w innej akcji, np. Nie możesz napisać:
main = do
line <- getLine
show (length line) -- Error:
-- Expected type: IO ()
-- Actual type: String
ponieważ show (length line)
ma typ, String
podczas gdy notacja wymaga, aby po akcji ( getLine
typu IO String
) następowała kolejna akcja (np. putStrLn (show (length line))
typu IO ()
).
EDYCJA 2
Definicja przejrzystości referencyjnej Jörga W. Mittaga jest bardziej ogólna niż moja (poparłem jego odpowiedź). Użyłem ograniczonej definicji, ponieważ przykład w pytaniu koncentruje się na wartości zwracanej funkcji i chciałem zilustrować ten aspekt. Jednak RT ogólnie odnosi się do znaczenia całego programu, w tym zmian stanu globalnego i interakcji ze środowiskiem (IO) spowodowanych oceną wyrażenia. Tak więc, aby uzyskać poprawną, ogólną definicję, należy odnieść się do tej odpowiedzi.