Посилальна прозорість, на яку посилається функція, вказує на те, що можна визначити результат застосування цієї функції, лише переглянувши значення її аргументів. Ви можете писати референтно прозорі функції будь-якою мовою програмування, наприклад, Python, Scheme, Pascal, C.
З іншого боку, у більшості мов можна також писати нереференційно прозорі функції. Наприклад, ця функція Python:
counter = 0
def foo(x):
global counter
counter += 1
return x + counter
не є референційно прозорим, насправді закликає
foo(x) + foo(x)
і
2 * foo(x)
дасть різні значення для будь-якого аргументу x
. Причиною цього є те, що функція використовує та змінює глобальну змінну, тому результат кожного виклику залежить від цього змінного стану, а не лише від аргументу функції.
Haskell, суто функціональна мова, суворо відокремлює оцінку вираження, в якій застосовуються чисті функції і завжди референтно прозорі, від виконання дій (обробки спеціальних значень), що не є референційно прозорим, тобто виконання однієї і тієї ж дії може мати щоразу різний результат.
Отже, для будь-якої функції Haskell
f :: Int -> Int
і будь-яке ціле число x
, це завжди правда
2 * (f x) == (f x) + (f x)
Приклад дії - результат функції бібліотеки getLine
:
getLine :: IO String
В результаті оцінки вираження ця функція (фактично константа) насамперед виробляє чисте значення типу IO String
. Цінністю цього типу є значення, як і будь-яке інше: ви можете передавати їх навколо, поміщати в структури даних, складати їх за допомогою спеціальних функцій тощо. Наприклад, ви можете скласти список таких дій:
[getLine, getLine] :: [IO String]
Дії особливі тим, що ви можете сказати виконанню Haskell виконувати їх, написавши:
main = <some action>
У цьому випадку, коли ваша програма Haskell запускається, час виконання проходить через дію, пов'язану з нею main
і виконує її, можливо, створюючи побічні ефекти. Отже, виконання дій не є прозоро прозорим, оскільки виконання однієї і тієї ж дії два рази може давати різні результати залежно від того, який час виконання буде отриманий як вхідний.
Завдяки системі типів Haskell, дія ніколи не може бути використана в контексті, коли очікується інший тип, і навпаки. Отже, якщо ви хочете знайти довжину рядка, ви можете скористатися length
функцією:
length "Hello"
повернеться 5. Але якщо ви хочете знайти довжину рядка, прочитаного з терміналу, ви не можете записати
length (getLine)
тому що ви отримуєте помилку типу: length
очікує введення списку типів (а String - це, справді, список), але getLine
є значенням типу IO String
(дії). Таким чином система типів забезпечує, що таке значення типу дії getLine
(виконання якого виконується за межами основної мови і яке може бути нереференційно прозорим) не може бути приховане всередині значення типу без дії Int
.
EDIT
Щоб відповісти на запитання, ось невелика програма Haskell, яка читає рядок з консолі та друкує її довжину.
main :: IO () -- The main program is an action of type IO ()
main = do
line <- getLine
putStrLn (show (length line))
Основна дія складається з двох відсівів, які виконуються послідовно:
getline
типу IO String
,
- другий будується шляхом оцінки функції
putStrLn
типу String -> IO ()
на його аргументі.
Точніше, друга дія будується
- прив’язка
line
до значення, прочитаного першою дією,
- оцінку чистих функцій
length
(обчислити довжину як ціле число), а потім show
(перетворити ціле число на рядок),
- побудова дії, застосувавши функцію
putStrLn
до результату show
.
У цей момент може бути виконана друга дія. Якщо ви набрали "Привіт", він надрукує "5".
Зауважте, що якщо ви отримуєте значення з дії, використовуючи <-
позначення, ви можете використовувати це значення лише в іншій дії, наприклад, ви не можете записати:
main = do
line <- getLine
show (length line) -- Error:
-- Expected type: IO ()
-- Actual type: String
тому що show (length line)
має тип, String
тоді як позначення do вимагає, щоб за дією ( getLine
типу IO String
) слідувала інша дія (наприклад, putStrLn (show (length line))
типу IO ()
).
EDIT 2
Визначення Йорґа Міттага щодо референтної прозорості є більш загальним, ніж моє (я підтримав його відповідь). Я використовував обмежене визначення, оскільки приклад у питанні зосереджений на зворотному значенні функцій, і я хотів проілюструвати цей аспект. Однак RT загалом посилається на значення всієї програми, включаючи зміни до глобального стану та взаємодії із середовищем (IO), спричинені оцінкою вираження. Отже, для правильного загального визначення слід звернутися до цієї відповіді.