Що таке референтна прозорість?


38

Я це бачив у імперативних парадигмах

f (x) + f (x)

може не збігатися з:

2 * f (x)

Але у функціональній парадигмі вона повинна бути однаковою. Я намагався реалізувати обидва випадки в Python та Scheme , але для мене вони виглядають досить просто.

Що може бути прикладом, який міг би вказати на різницю із заданою функцією?


7
Ви можете, і часто це робите, записувати референтно прозорі функції в python. Різниця в тому, що мова її не виконує.
Карл Білефельдт

5
в C і подібних: f(x++)+f(x++)можливо, це не те саме, що 2*f(x++)(у C це особливо прекрасно, коли подібні речі приховані в макросах - чи я зламав ніс на цьому? ти ставиш)
gnat

Наскільки я розумію, приклад @ gnat полягає в тому, чому функціонально орієнтовані мови, такі як R, використовують пропускне посилання та явно уникають функцій, що змінюють їхні аргументи. Принаймні, R може бути важко спіднити ці обмеження (принаймні, стабільним, портативним способом), не заглиблюючись у складну систему мови, простори імен та шляхи пошуку.
shadowtalker

4
@ssdecontrol: Насправді, коли ви маєте референтну прозорість, значення прохідних і посилальних завдань завжди дають точно такий же результат, тому не має значення, яку саме мову використовує мова. Функціональні мови часто вказуються з чимось схожим на прохідне значення для смислової чіткості, але їх реалізація часто використовує пропускну посилання для продуктивності (або навіть обидві, залежно від того, яка з них швидша в заданому контексті).
Йорг W Міттаг,

4
@gnat: Зокрема, f(x++)+f(x++)може бути абсолютно будь-що, оскільки це викликає невизначеність поведінки. Але це насправді не пов’язано з референтною прозорістю - що не допомогло б для цього дзвінка, це "невизначено" для референтно прозорих функцій, як і в sin(x++)+sin(x++). Може бути 42, міг би відформатувати ваш жорсткий диск, демони могли вилетіти з носа користувачів…
Крістофер Кройцгіг,

Відповіді:


62

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

Основна дія складається з двох відсівів, які виконуються послідовно:

  1. getlineтипу IO String,
  2. другий будується шляхом оцінки функції putStrLnтипу String -> IO ()на його аргументі.

Точніше, друга дія будується

  1. прив’язка lineдо значення, прочитаного першою дією,
  2. оцінку чистих функцій length(обчислити довжину як ціле число), а потім show(перетворити ціле число на рядок),
  3. побудова дії, застосувавши функцію 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), спричинені оцінкою вираження. Отже, для правильного загального визначення слід звернутися до цієї відповіді.


10
Чи може підручник запропонувати, як я можу вдосконалити цю відповідь?
Джорджіо

Отже, як би отримати довжину рядка, прочитаного з терміналу в Haskell?
sbichenko

2
Це надзвичайно педантично, але задля повноти, це не система типу Хаскелла, яка забезпечує дії та чисті функції не змішуються; це той факт, що мова не забезпечує жодних нечистих функцій, які можна безпосередньо зателефонувати. Ви можете реально реалізувати IOтип Haskell на будь-якій мові з лямбдами та дженериками, але оскільки кожен може телефонувати printlnбезпосередньо, реалізація IOне гарантує чистоти; це була б просто конвенція.
Doval

Я мав на увазі, що (1) всі функції є чистими (звичайно, вони чисті, тому що мова не дає жодних нечистих, хоча, наскільки я знаю, існують деякі механізми, щоб обійти це), і (2) чисті функції і нечисті дії мають різні типи, тому їх не можна змішувати. До речі, що ви маєте на увазі під телефоном безпосередньо ?
Джорджіо

6
Ваша думка про getLineнепрозорість прозорості неправильна. Ви представляєте getLineтак, ніби він оцінює або зменшує до якоїсь струни, конкретна рядок якої залежить від введення користувача. Це неправильно. IO Stringбільше не містить рядка Maybe String. IO String- це рецепт для, можливо, отримання рядка, і, як вираз, він такий же чистий, як і будь-який інший в Haskell.
LuxuryMode

25
def f(x): return x()

from random import random
f(random) + f(random) == 2*f(random)
# => False

Однак, це не те, що означає референтна прозорість. RT означає, що ви можете замінити будь-який вираз у програмі результатом оцінки цього виразу (або навпаки), не змінюючи значення програми.

Візьмемо, наприклад, таку програму:

def f(): return 2

print(f() + f())
print(2)

Ця програма референтно прозора. Я можу замінити один або обидва входження f()з , 2і вона буде працювати так само:

def f(): return 2

print(2 + f())
print(2)

або

def f(): return 2

print(f() + 2)
print(2)

або

def f(): return 2

print(2 + 2)
print(f())

всі будуть поводитись однаково.

Ну, власне, я обдурила. Я повинен мати можливість замінити виклик на printйого повернене значення (яке зовсім не значення), не змінюючи значення програми. Однак, очевидно, якщо я просто видалю два printтвердження, зміст програми зміниться: раніше вона надрукувала щось на екрані, після цього не відбудеться. Введення / виведення не є прозорим.

Просте правило: якщо ви можете замінити будь-який вираз, підвираз або виклик підпрограми на зворотне значення цього виразу, підвираз або виклик підпрограми в будь-якій точці програми, не змінюючи її значення, програма має референційний характер прозорість. І що це означає, практично кажучи, що ви не можете мати жодного вводу-виводу, не мати будь-якого змінного стану, не мати жодних побічних ефектів. У кожному виразі значення виразу повинно залежати виключно від значень складових частин виразу. І в кожному виклику підпрограми повернене значення повинно залежати виключно від аргументів.


4
"не може мати стан, що змінюється": Ну, ви можете мати його, якщо він прихований і не впливає на поведінку коду, що спостерігається. Подумайте, наприклад, про запам'ятовування.
Джорджіо

4
@Giorgio: Це, мабуть, суб'єктивно, але я б стверджував, що кешовані результати насправді не є "змінним станом", якщо вони приховані і не мають помітних ефектів. Незмінюваність - це завжди абстракція, реалізована поверх апаратних засобів, що змінюються; часто це надається мовою (надаючи абстракцію "значення", навіть якщо значення може переміщуватися між регістрами та місцями пам'яті під час виконання, і може зникнути, коли стане відомо, воно більше ніколи не буде використане), але це не менш справедливо, коли це надана бібліотекою чи інше. (Якщо припустити, що він реалізований правильно, звичайно.)
ruakh

1
+1 Я дуже люблю printприклад. Можливо, один із способів побачити це - те, що надруковано на екрані, є частиною "поверненого значення". Якщо ви можете замінити printйого функцією зворотне значення та еквівалентну запис на терміналі, приклад працює.
П'єр Арло

1
@Giorgio Використання простору / часу не може вважатися побічним ефектом для цілей еталонної прозорості. Це зробило б 4і 2 + 2не взаємозамінні, оскільки вони мають різний час роботи, і вся суть референтної прозорості полягає в тому, що ви можете замінити вираз тим, що воно оцінює. Важливою увагою буде безпека ниток.
Doval

1
@overexchange: Референтна прозорість означає, що ви можете замінити кожну підекспресію на її значення, не змінюючи значення програми. listOfSequence.append(n)повертається None, так що ви повинні бути в змозі замінити кожен виклик listOfSequence.append(n)з Noneбез зміни сенсу програми. Ви можете це зробити? Якщо ні, то це не референтно прозоро.
Йорг W Міттаг

1

Частини цієї відповіді взяті безпосередньо з незакінченого підручника з функціонального програмування , розміщеного на моєму акаунті GitHub:

Кажуть, що функція є відносно прозорою, якщо вона, задана однаковими вхідними параметрами, завжди дає однаковий вихід (повернене значення). Якщо хтось шукає причину чистого функціонального програмування, референтна прозорість є хорошим кандидатом. Якщо міркувати з формулами алгебри, арифметики та логіки, ця властивість, яку також називають заміщенням рівних рівним, є настільки принципово важливою, що її зазвичай приймають як належне ...

Розглянемо простий приклад:

x = 42

У чистому функціональному мові ліва і права частина знака рівності є замінними один для одного обома способами. Тобто, на відміну від такої мови, як C, зазначені вище позначення справді стверджують рівність. Наслідком цього є те, що ми можемо міркувати про програмний код так само, як математичні рівняння.

З Вікі Haskell :

Чисті обчислення дають однакове значення щоразу, коли вони викликаються. Ця властивість називається референтною прозорістю і дає можливість проводити вирівнюючі міркування щодо коду ...

На противагу цьому, тип операцій, що виконуються C-подібними мовами, іноді називають деструктивним призначенням .

Термін чистий часто використовується для опису властивості виразів, що стосується даної дискусії. Щоб функцію вважали чистою,

  • заборонено проявляти будь-які побічні ефекти;
  • він повинен бути референтно прозорим.

Відповідно до метафори чорного поля, знайденої в численних підручниках з математики, внутрішні функції функції повністю запечатані від зовнішнього світу. Побічним ефектом є те, коли функція або вираз порушує цей принцип - тобто процедурі дозволено якимось чином спілкуватися з іншими програмними підрозділами (наприклад, обмінюватися та обмінюватися інформацією).

Підсумовуючи, референтна прозорість є необхідною для функцій, які поводяться як справжні , математичні функції також у семантиці мов програмування.


це, здається, відкривається за допомогою копії слово-в-слово, взятого звідси : "Функція, як кажуть, є прозоро прозорою, якщо вона, враховуючи однакові вхідні параметри, завжди видає однаковий вихід ..." Stack Exchange має правила для плагіату , є вам відомо про це? "Плагіатство - це бездушний акт копіювати шматки чужої роботи, ляпати її своїм ім'ям і передавати себе як оригінального автора ..."
гнат

3
Я написав цю сторінку.
yesthisisuser

якщо це так, подумайте про те, щоб це виглядало менш плагіатом - адже читачам немає способу сказати. Чи знаєте ви, як це зробити в SE? 1) Ви посилаєтесь на оригінальне джерело, наприклад "Як (я) написав [here](link to source)..." з подальшим 2) правильним форматуванням цитат (використовуйте для цього лапки або ще краще - > символ). Також не завадить, якщо крім загальних вказівок, відповіді на конкретні запитання про це, в даному випадку про f(x)+f(x)/ 2*f(x), див. Як відповісти - інакше може виглядати так, що ви просто рекламуєте свою сторінку
gnat

1
Теоретично я зрозумів цю відповідь. Але, практично дотримуючись цих правил, мені потрібно повернути список послідовностей градів у цій програмі . Як це зробити?
переоцінка
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.