Чи вважаються закриття нечистим функціональним стилем?


33

Чи закриття вважається нечистим у функціональному програмуванні?

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

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

Одним із критеріїв чистої функції є те, що "Функція завжди оцінює одне і те ж значення результату, даючи однакові значення аргументів ."

Припустимо

f: x -> x + y

f(3)не завжди дасть однаковий результат. f(3)залежить від значення, yяке не є аргументом f. Таким чином, fце не є чистою функцією.

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

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

Чи я правильно думаю про це?


6
Я весь час використовую закриття в Haskell, і Haskell настільки ж чистий, як це виходить.
Томас Едінг

5
Чистою функціональною мовою yзмінити не можна, тому вихідний результат f(3)завжди буде однаковим.
Лілі Чунг

4
yє частиною визначення, fнавіть якщо воно не позначене явно як вхід до f- це все-таки випадок, який fвизначається в термінах y(ми можемо позначити функцію f_y, зробити залежність від yявного), і тому зміна yдає іншу функцію . Конкретна функція, f_yвизначена для конкретної y, дуже чиста. (Наприклад, ці дві функцій f: x -> x + 3і f: x -> x + 5мають різні функції, і як чисто, навіть якщо ми випадково використовувати ту ж букву для позначення їх.)
ShreevatsaR

Відповіді:


26

Чистоту можна виміряти двома речами:

  1. Чи завжди функція повертає один і той же вихід, заданий однаковим входом; тобто це референтно прозоро?
  2. Чи змінює функція щось поза себе, тобто чи має побічні ефекти?

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


Чи не детермінізм першого пункту? Або це теж частина чистоти? Я не надто знайомий з поняттям "чистота" в контексті програмування.

4
@JimmyHoffa: Не обов’язково. Ви можете помістити висновок апаратного таймера у функцію, і нічого поза функцією не буде змінено.
Роберт Харві

1
@RobertHarvey Це все про те, як ми визначаємо входи до функції? Моя цитата з wikipedia зосереджена на аргументах функцій, тоді як ви додатково розглядаєте закриті змінну (и) як вхідні дані.
user2179977

8
@ User2179977: якщо вони не змінюються, ви повинні НЕ розглядати замкнуті поверх змінних в якості додаткових входів для функції. Швидше, ви повинні вважати, що саме закриття є функцією, а іншою функцією, коли воно закривається на інше значення y. Так, наприклад, ми визначаємо функцію gтаку, яка g(y)сама є функцією x -> x + y. Тоді gце функція цілих чисел, яка повертає функції, g(3)це функція цілих чисел, яка повертає цілі числа, і g(2)є іншою функцією цілих чисел, яка повертає цілі числа. Всі три функції чисті.
Стів Джессоп

1
@Darkhogg: Так. Дивіться моє оновлення.
Роберт Харві

10

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

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

Уявіть собі це (псевдокод):

foo(x) {
    let y = x + 1
    ...
}

yє цінністю. Його значення залежить від x, але xнезмінне, тому yзначення також є незмінним. Ми можемо викликати fooбагато разів різними аргументами, які дадуть різні результати y, але yвсі вони живуть в різних сферах і залежать від різних xs, тому чистота залишається недоторканою.

Тепер давайте змінимо:

bar(x) {
    let y(z) = x + z
    ....
}

Тут ми використовуємо закриття (закриваємось над x), але це так само, як і в foo- різні виклики barз різними аргументами створюють різні значення y(пам'ятайте - функції є значеннями), які всі незмінні, тому чистота залишається незмінною.

Крім того, зауважте, що закриття мають дуже схожий вплив на отримання каррі:

adder(a)(b) {
    return a + b
}
baz(x) {
    let y = adder(x)
    ...
}

bazнасправді не відрізняється від того bar- і в обох ми створюємо значення функції, yяке повертає його аргумент плюс x. Насправді, в обчисленні Lambda ви використовуєте закриття для створення функцій з декількома аргументами - і це все ще не є нечистим.


9

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

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

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

Скажімо, у вас є такий фрагмент коду, як цей:

let make y =
    fun x -> x + y

Виклик make 3і make 4дасть вам дві функції з закриттям над make«S yаргумент. Один із них повернеться x + 3, інший x + 4. Однак вони мають дві різні функції, і обидві є чистими. Вони були створені за допомогою тієї ж makeфункції, але це все.

Зверніть увагу на більшу частину часу назад.

  1. У Haskell, який є чистим, ви можете закрити лише незмінні значення. Немає змінного стану, який би закривався. Ви впевнені, що отримаєте чисту функцію таким чином.
  2. У нечистих функціональних мовах, таких як F #, ви можете закрити посилання на осередки та типи посилань і отримати діючу функцію. Ви маєте рацію в тому, що вам слід відстежувати сферу, в якій визначена функція, щоб знати, чи це чи ні. Ви можете легко сказати, чи є значення змінним у цих мовах, тому це не є великою проблемою.
  3. У мовах OOP, які підтримують закриття, як-от C # і JavaScript, ситуація схожа на нечисті функціональні мови, але відстеження зовнішньої області стає більш хитрою, оскільки змінні за замовчуванням змінюються.

Зауважте, що для 2 та 3 ці мови не гарантують чистоту. Домішка там не властивість закриття, а самої мови. Закриття не змінюють картину самостійно.


1
Ви можете абсолютно закрити значення, що змінюються, в Haskell, але така річ буде зазначатися монадою IO.
Даніель Гратцер

1
@jozefg ні, ви закриваєте незмінне IO Aзначення, і ваш тип закриття є IO (B -> C)чи дещо. Чистота підтримується
Калет

5

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

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

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

EDIT: Що стосується власного редагування / прикладу ...

Припустимо

f: x -> x + y

f (3) не завжди дасть однаковий результат. f (3) залежить від значення y, яке не є аргументом f. Таким чином, f не є чистою функцією.

Тут залежить - неправильний вибір слова. Цитуючи ту саму статтю у Вікіпедії, яку ви робили:

У комп'ютерному програмуванні функція може бути описана як чиста функція, якщо обидва ці твердження про функцію виконуються:

  1. Функція завжди оцінює одне і те саме значення результату, задаючи однакові значення аргументів. Значення результату функції не може залежати від будь-якої прихованої інформації або стану, яка може змінюватися в ході виконання програми або між різними виконанням програми, а також не може залежати від зовнішнього вводу пристроїв вводу-виводу.
  2. Оцінка результату не спричиняє жодних семантично помітних побічних ефектів або результатів, таких як мутація змінних об'єктів або вихід на пристрої вводу / виводу.

Якщо припустити, що yце незмінне (що зазвичай буває у функціональних мовах), умова 1 виконується: для всіх значень xзначення f(x)не змінюється. Це повинно бути зрозуміло з того, що yнічим не відрізняється від постійної, і x + 3є чистою. Зрозуміло також, що ніяких мутацій або вводу-виводу не відбувається.


3

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

Тепер поговоримо про закриття.

Нудні (здебільшого чисті) "закриття"

Закриття трапляються тому, що, оцінюючи лямбда-термін, ми інтерпретуємо (зв'язані) змінні як пошукові середовища. Таким чином, коли ми повернемо лямбда-термін як результат оцінки, змінні, що знаходяться всередині нього, будуть "закритими" значеннями, прийнятими при визначенні.

У простому лямбдальному обчисленні це щось тривіальне, і все поняття просто зникає. Щоб продемонструвати це, ось відносно легкий перекладач обчислення лямбда:

-- untyped lambda calculus values are functions
data Value = FunVal (Value -> Value)

-- we write expressions where variables take string-based names, but we'll
-- also just assume that nobody ever shadows names to avoid having to do
-- capture-avoiding substitutions

type Name = String

data Expr
  = Var Name
  | App Expr Expr
  | Abs Name Expr

-- We model the environment as function from strings to values, 
-- notably ignoring any kind of smooth lookup failures
type Env = Name -> Value

-- The empty environment
env0 :: Env
env0 _ = error "Nope!"

-- Augmenting the environment with a value, "closing over" it!
addEnv :: Name -> Value -> Env -> Env
addEnv nm v e nm' | nm' == nm = v
                  | otherwise = e nm

-- And finally the interpreter itself
interp :: Env -> Expr -> Value
interp e (Var name) = e name          -- variable lookup in the env
interp e (App ef ex) =
  let FunVal f = interp e ef
      x        = interp e ex
  in f x                              -- application to lambda terms
interp e (Abs name expr) =
  -- augmentation of a local (lexical) environment
  FunVal (\value -> interp (addEnv name value e) expr)

Важлива частина, яку слід помітити, - це addEnvколи ми доповнюємо середовище новою назвою. Ця функція називається лише "всередині" інтерпретованого Absтерміну тяги (лямбда-термін). Навколишнє середовище стає "придивленим" кожного разу, коли ми оцінюємо Varтермін, і таким чином вони Varвирішують будь-що, про що Nameйдеться в тому, Envщо потрапило в полон до Absтяги, що містить Var.

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

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

-- in simple LC syntax:
--
-- (\x -> (x x)) (\x -> (x x))
omega :: Expr
omega = App (Abs "x" (App (Var "x") 
                          (Var "x")))
            (Abs "x" (App (Var "x") 
                          (Var "x")))

Цікаві (нечисті) закриття

Тепер для певних фонів закриття, описані в простому LC вище, нудні, тому що не існує можливості взаємодіяти зі змінними, які ми закрили. Зокрема, слово "закриття", як правило, викликає такий код, як наступний Javascript

> function mk_counter() {
  var n = 0;
  return function incr() {
    return n += 1;
  }
}
undefined

> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3

Це свідчить про те, що ми закрили nзмінну у внутрішній функції, incrа виклик incrзмістовно взаємодіє із цією змінною. mk_counterє чистою, але incr, безумовно, нечистою (а також не відносно прозорою).

Чим відрізняються ці два випадки?

Поняття "змінна"

Якщо ми подивимось на те, що означають підміна і абстракція в простому ЖК-значенні, то помітимо, що вони, безумовно, звичайні. Змінні - це не що інше, як безпосереднє пошуку довкілля. Абстракція лямбда - це не що інше, як створення доповненого середовища для оцінки внутрішнього вираження. У цій моделі немає місця для того, з якою поведінкою ми бачились mk_counter/ incrтому, що жодні зміни не дозволені.

Для багатьох це серце того, що означає "змінна" - варіація. Однак семантики люблять розрізняти тип змінної, що використовується в LC, і тип "змінної", що використовується в Javascript. Для цього вони, як правило, називають останню "змінною коміркою" або "слотом".

Ця номенклатура слідує за довгим історичним використанням "змінної" в математиці, де вона означала щось більше на кшталт "невідомого": (математичний) вираз "x + x" не дозволяє xзмінюватись у часі, а замість цього має означати значення значення (одиничне, постійне) xприймає.

Таким чином, ми говоримо "слот", щоб підкреслити здатність ставити значення в слот і виводити їх.

Щоб додати ще плутанину, у Javascript ці "слоти" виглядають так само, як і змінні: ми пишемо

var x;

створити його і тоді, коли ми пишемо

x;

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

-- create a fresh, empty slot and name it `x` in the context of the 
-- expression E
let x = newSlot in E

-- look up the value stored in the named slot named `x`, return that value
get x

-- store a new value, `v`, in the slot named `x`, return the slot
put x v

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

Використовуючи це позначення, ми можемо переписати mk_counterприклад (на цей раз у синтаксисі, подібному до Haskell, хоч і рішуче не в Haskell-подібній семантиці):

mkCounter = 
  let x = newSlot 
  in (\() -> let old = get x 
             in get (put x (old + 1)))

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

Знову ж таки, mkCounterдуже нечисто. Це також дуже референтно непрозоро. Але зауважте, що побічні ефекти виникають не з захоплення і закриття імені, а замість захоплення змінної комірки та побічних операцій над нею як getі put.

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

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


1

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

Зауважте, що хоча ви завжди можете передавати значення як аргумент, зазвичай це не обійтися без значних труднощів. Наприклад (coffeescript):

closedValue = 42
return (arg) -> console.log "#{closedValue} #{arg}"

За вашою пропозицією ви можете просто повернутися:

return (arg, closedValue) -> console.log "#{closedValue} #{arg}"

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

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

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