Побічні ефекти, що порушують референтну прозорість


11

Функціональне програмування в Scala пояснює вплив побічного ефекту на порушення референтної прозорості:

побічний ефект, який передбачає деяке порушення референсної прозорості.

Я прочитав частину SICP , в якій обговорюється використання „моделі заміщення” для оцінки програми.

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

Однак, як зазначено у наведеній цитаті, використання побічних ефектів може / порушить модель заміни.

Приклад:

val x = foo(50) + bar(10)

Якщо fooі bar не мають побічних ефектів, то виконання будь-якої функції завжди поверне той самий результат x. Але, якщо вони мають побічні ефекти, вони змінять змінну, яка порушує / кидає гайковий ключ у модель заміни.

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

Будь ласка, виправте мене та заповніть будь-які діри щодо побічних ефектів, що порушують RT, обговорюючи також вплив на модель заміни.

Відповіді:


20

Почнемо з визначення еталонної прозорості :

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

Це означає, що (наприклад) ви можете замінити 2 + 5 на 7 в будь-якій частині програми, і програма все одно повинна працювати. Цей процес називається заміщенням. Заміна діє, якщо і лише тоді, коли 2 + 5 можна замінити на 7, не впливаючи на будь-яку іншу частину програми .

Скажімо, у мене клас, який називається Baz, з функціями Fooі Barв ньому. Для простоти ми просто скажемо, що Fooі Barобидва повертають передане значення. Отже Foo(2) + Bar(5) == 7, як ви і очікували. Референтна прозорість гарантує, що ви можете замінити вираз Foo(2) + Bar(5)виразом у 7будь-якій точці вашої програми, і програма все одно буде функціонувати однаково.

Але що робити, якщо Fooповернуто передане значення, але Barповернене передане значення плюс останнє значення, надане Foo? Це досить просто зробити, якщо ви зберігаєте значення Fooв локальній змінній у Bazкласі. Добре, якщо початкове значення цієї локальної змінної дорівнює 0, вираз Foo(2) + Bar(5)поверне очікуване значення 7в перший раз, коли ви викликаєте його, але воно поверне 9другий раз, коли ви викликаєте його.

Це порушує прозору прозорість двома способами. По-перше, на Бар не можна розраховувати повертати один і той же вираз кожного разу, коли він викликається. По-друге, відбувся побічний ефект, а саме те, що виклик Foo впливає на повернене значення Bar. Оскільки ви вже не можете гарантувати, що Foo(2) + Bar(5)дорівнює 7, ви більше не можете її замінити.

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


5
Таким чином, порушення роботи RTзабороняє вас від використання substitution model.великої проблеми з тим, що не в змозі скористатися substitution model- це сила її використання для міркування про програму?
Кевін Мередіт

Це точно так.
Роберт Харві

1
+1 чудово чітка і зрозуміла відповідь. Дякую.
Racheet

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

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

3

Уявіть, що ви намагаєтеся побудувати стіну, і вам надали асортимент ящиків різного розміру та форми. Вам потрібно заповнити певний отвір у формі Г у стіні; ви повинні шукати коробку у формі Г або можете замінити дві прямі коробки відповідного розміру?

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

В імперативному світі небезпечно будувати стіну, не перевіряючи вміст кожної скриньки та порівнюючи їх із вмістом кожного іншого коробки:

  • Деякі містять сильні магніти і, якщо неправильно вирівняні, виштовхують інші магнітні коробки зі стіни.
  • Деякі дуже гарячі або холодні і погано реагуватимуть, якщо їх розмістити в сусідніх приміщеннях.

Я думаю, що я зупинюсь, перш ніж витрачати час на більш неправдоподібні метафори, але сподіваюся, що справа зроблена; функціональна цегла не містить прихованих сюрпризів і цілком передбачувана. Оскільки ви завжди можете використовувати менші блоки потрібного розміру та форми для заміни більшого, і немає різниці між двома полями однакового розміру та форми, ви маєте референтну прозорість. З імперативних цеглин недостатньо мати щось потрібного розміру та форми - ви повинні знати, як цегла була побудована. Не відносно прозорий.

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

Імперативною мовою ви ніколи не знаєте, які сюрпризи можуть ховатися всередині.


"У чистому функціональному мові все, що вам потрібно побачити, - це підпис функції, щоб знати, що вона робить." - Це взагалі не так. Так, за припущенням параметричного поліморфізму ми можемо зробити висновок, що функцією типу (a, b) -> aможе бути лише fstфункція, а функція типу a -> aможе бути лише identityфункцією, але не можна обов'язково нічого говорити про функцію типу (a, a) -> a.
Йорг W Міттаг

2

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

Так, інтуїція цілком права. Ось кілька покажчиків, щоб отримати більш точні дані:

Як ви сказали, будь-який вираз RT повинен мати single"результат". Тобто, даючи factorial(5)вираз у програмі, він завжди повинен давати однаковий «результат». Отже, якщо певна factorial(5)частина програми, і вона дає 120, вона завжди має 120, незалежно від того, який "порядок кроків" він розширюється / обчислюється - незалежно від часу .

Приклад: factorialфункція.

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

Є кілька міркувань щодо цього пояснення.

Перш за все, пам’ятайте, що різні моделі оцінювання (див. Прикладний та звичайний порядок) можуть давати різні «результати» для одного виразу RT.

def first(y, z):
  return y

def second(x):
  return second(x)

first(2, second(3)) # result depends on eval. model

У наведеному вище коді, firstі secondє референціально прозорими, і тим НЕ менш, вираз в кінці дає різні «результати» , якщо оцінені при нормальному порядку і аплікативного порядку ( в відповідності з останніми, вираз не зупиняється).

.... що призводить до використання "результату" в лапках. Оскільки для припинення виразу не потрібно, воно може не спричинити значення. Тож використання "результату" - це свого роду розмиття. Можна сказати, що вираз RT завжди computationsотримує те саме в моделі оцінки.

По-третє, може знадобитися побачити два програми, які foo(50)з’являються в програмі в різних місцях, як різні вирази - кожен з яких дає свої власні результати, які можуть відрізнятися один від одного. Наприклад, якщо мова дозволяє динамічну область застосування, обидва вирази, хоча і лексично однакові, є різними. У перл:

sub foo {
    my $x = shift;
    return $x + $y; # y is dynamic scope var
}

sub a {
    local $y = 10;
    return &foo(50); # expanded to 60
}

sub b {
    local $y = 20;
    return &foo(50); # expanded to 70
}

Динамічний обсяг вводить в оману, оскільки він полегшує думці, що xце єдиний внесок для foo, коли насправді це є xі є y. Один із способів побачити різницю - перетворити програму в еквівалентну без динамічної області застосування - тобто, явно передаючи параметри, тому замість визначення foo(x)ми визначаємо foo(x, y)та передаємо yявно в абонентах.

Справа в тому, що ми завжди functionналаштовані на думку: даючи певний вклад для виразу, нам дають відповідний "результат". Якщо ми даємо однаковий внесок, ми завжди повинні очікувати однакового "результату".

А що з наступним кодом?

def foo():
   global y
   y = y + 1
   return y

y = 10
foo() # yields 11
foo() # yields 12

fooПроцедура ламає RT , тому що є перевизначення. Тобто ми визначилися yв одному пункті, а в останньому - переозначили це саме y . У прикладі perl, описаного вище, ys є різними прив'язками, хоча вони мають однакову буквену назву "y". Тут yфактично те саме. Ось чому ми кажемо (пере) призначення - це мета- операція: ви насправді змінюєте визначення своєї програми.

Приблизно люди зазвичай відображають різницю наступним чином: у вільній налаштуваннях для побічних ефектів ви маєте карту input -> output. У "імперативній" обстановці у вас є input -> ouputконтекст, stateякий може змінюватися з часом.

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

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


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