Посилальна прозорість, на яку посилається функція, вказує на те, що можна визначити результат застосування цієї функції, лише переглянувши значення її аргументів. Ви можете писати референтно прозорі функції будь-якою мовою програмування, наприклад, 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), спричинені оцінкою вираження. Отже, для правильного загального визначення слід звернутися до цієї відповіді.