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

33

Являются ли замыкания нечистыми в функциональном программировании?

Кажется, что можно вообще избежать замыканий, передавая значения непосредственно в функцию. Поэтому следует ли по возможности избегать закрытия?

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

Один из критериев для чистой функции заключается в том, что «функция всегда оценивает одно и то же значение результата при одинаковых значениях аргумента ».

предполагать

f: x -> x + y

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

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

Это приводит меня к тому, что одна и та же функция может быть чистой в одной ситуации, но нечистой в другой. Не всегда можно определить, является ли функция чистой или нет, изучая ее исходный код. Скорее, может потребоваться рассмотреть его в контексте его окружения в тот момент, когда он вызывается, прежде чем можно будет провести такое различие.

Я правильно об этом думаю?

user2179977
источник
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 Это все о том, как мы определяем входные данные для функции? Моя цитата из Википедии фокусируется на аргументах функции, тогда как вы дополнительно рассматриваете закрытую переменную (и) в качестве входных данных.
user2179977
8
@ user2179977: если они не изменяемы, закрытые переменные не следует рассматривать как дополнительные входные данные для функции. Скорее, вы должны рассматривать само замыкание как функцию и как другую функцию, когда оно закрывается по другому значению y. Так, например, мы определяем функцию g, которая g(y)сама является функцией x -> x + y. Тогда gесть функция , которая возвращает целые числа функций, g(3)является функцией , которая возвращает целые числа, и g(2)это другая функция , которая возвращает целые числа. Все три функции чистые.
Стив Джессоп
1
@ Дархогг: Да. Смотрите мое обновление.
Роберт Харви
10

Появляются замыкания Lambda Calculus, которые являются самой чистой формой функционального программирования, поэтому я бы не назвал их «нечистыми» ...

Замыкания не являются «нечистыми», потому что функции в функциональных языках являются гражданами первого сорта - это означает, что их можно рассматривать как ценности.

Представьте себе это (псевдокод):

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

yэто значение. Это значение зависит от x, но xявляется неизменным, поэтому yзначение также является неизменным. Мы можем вызывать fooмного раз с разными аргументами, которые приведут к различным ys, но 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 Calculus вы используете замыкания для создания функций с несколькими аргументами - и это все еще не является нечистым.

Идан Арье
источник
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. В языках ООП, которые поддерживают замыкания, таких как C # и JavaScript, ситуация аналогична нечистым функциональным языкам, но отслеживание внешней области действия становится более сложным, поскольку переменные по умолчанию являются изменяемыми.

Обратите внимание, что для 2 и 3 эти языки не дают никаких гарантий относительно чистоты. Примесь там не является свойством замыкания, но самого языка. Закрытия сами по себе не сильно меняют картину.

scrwtp
источник
1
Вы можете абсолютно закрывать изменяемые значения в Haskell, но такая вещь будет аннотирована монадой ввода-вывода.
Даниэль Гратцер
1
@jozefg нет, вы закрываете неизменяемое IO Aзначение, и ваш тип закрытия является IO (B -> C)или что-то подобное. Чистота поддерживается
Caleth
5

Обычно я прошу вас уточнить ваше определение «нечистого», но в этом случае это не имеет значения. Предполагая, что вы противопоставляете его термину « чисто функциональный» , ответ «нет», потому что нет ничего о замыканиях, которые по своей природе разрушительны. Если бы ваш язык был чисто функциональным без замыканий, он все равно был бы чисто функциональным с замыканиями. Если вместо этого вы имеете в виду «не функционально», ответ по-прежнему «нет»; замыкания облегчают создание функций.

Кажется, что можно вообще избежать замыканий, передавая данные непосредственно в функцию.

Да, но тогда у вашей функции будет еще один параметр, и это изменит его тип. Замыкания позволяют создавать функции на основе переменных без добавления параметров. Это полезно, например, когда у вас есть функция, которая принимает 2 аргумента, и вы хотите создать ее версию, которая принимает только 1 аргумент.

РЕДАКТИРОВАТЬ: Что касается вашего собственного редактирования / пример ...

предполагать

f: x -> x + y

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

Зависит от неправильного выбора слова здесь. Цитирую ту же статью в Википедии, что и вы:

В компьютерном программировании функция может быть описана как чистая функция, если оба эти утверждения о функции выполняются:

  1. Функция всегда оценивает одно и то же значение результата, учитывая одно и то же значение аргумента. Значение результата функции не может зависеть от какой-либо скрытой информации или состояния, которые могут изменяться по мере выполнения программы или между различными выполнениями программы, а также не может зависеть от какого-либо внешнего ввода от устройств ввода-вывода.
  2. Оценка результата не вызывает какого-либо семантически наблюдаемого побочного эффекта или вывода, такого как мутация изменяемых объектов или вывод на устройства ввода-вывода.

Предполагая, что yявляется неизменным (что обычно имеет место в функциональных языках), условие 1 выполняется: для всех значений xзначение f(x)не изменяется. Это должно быть ясно из того факта, что yничем не отличается от константы и x + 3является чистым. Также ясно, что здесь нет мутаций или операций ввода-вывода.

Doval
источник
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явно нечисто (и референтно не прозрачно).

Чем отличаются эти два случая?

Понятия "переменная"

Если мы посмотрим, что означают подстановка и абстракция в простом смысле слова LC, мы заметим, что они явно просты. Переменные буквально не более чем непосредственный поиск окружения. Лямбда-абстракция - это буквально не что иное, как создание расширенной среды для оценки внутреннего выражения. В этой модели нет места для того поведения, которое мы видели с 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.

В конечном счете, я думаю, что это окончательный ответ на ваш вопрос: на чистоту не влияет (математический) захват переменных, а вместо этого побочные эффекты, выполняемые с изменяемыми слотами, названными захваченными переменными.

Только в тех языках, которые не пытаются быть близкими к ЛНР или не пытаются поддерживать чистоту, эти два понятия так часто смешиваются, что приводит к путанице.

Дж. Абрахамсон
источник
1

Нет, замыкания не приводят к нечистоте функции, пока закрытое значение является постоянным (не изменяется ни замыканием, ни другим кодом), что является обычным случаем в функциональном программировании.

Обратите внимание, что, хотя вы всегда можете вместо этого передать значение в качестве аргумента, обычно вы не можете сделать это без значительных трудностей. Например (coffeescript):

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

По вашему предложению вы можете просто вернуться:

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

Эта функция не называется в этой точке, просто определено , так что вам придется найти способ , чтобы передать нужное значение для closedValueточки , в которой функция фактически называется. В лучшем случае это создает много связей. В худшем случае вы не контролируете код в точке вызова, поэтому это практически невозможно.

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

Карл Билефельдт
источник