Чистые функции: означает ли «отсутствие побочных эффектов» «всегда одинаковый результат при одинаковом вводе»?

84

Два условия, которые определяют функцию pure, следующие:

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

Если первое условие всегда верно, бывает ли когда-нибудь второе условие неверно?

Т.е. действительно ли это необходимо только с первым условием?

Магнус
источник
3
Ваше помещение не определено. "Вход" слишком широк. Можно подумать, что у двух функций есть типы ввода. Их аргументы и «экологические» / «контекстные». Функцию, возвращающую системное время, можно было бы считать чистой (даже если это не так), если вы не различаете эти два типа ввода.
Александр
4
@Alexander: В контексте «чистой функции» «ввод» обычно понимается как означающий параметры / аргументы, которые передаются явно (независимо от механизма, используемого языком программирования). Это часть определения «чистой функции». Но вы правы, важно помнить определение.
sleske 05
3
Тривиальный контрпример: вернуть значение глобальной переменной. Никаких побочных эффектов (глобальный только когда-либо читается!), Но потенциально разные результаты каждый раз. (Если вам не нравятся глобальные переменные, верните адрес локальной переменной, которая зависит от стека вызовов во время выполнения).
Питер - Восстановите Монику
2
Вам необходимо расширить определение «побочных эффектов»; вы говорите, что чистый метод не вызывает побочных эффектов, но вы также должны отметить, что чистый метод не потребляет побочных эффектов, произведенных где-либо еще.
Эрик Липперт
2
@sleske Возможно, обычно понимается, но отсутствие этого различия является точной причиной путаницы OP.
Александр

Ответы:

114

Вот несколько контрпримеров, которые не меняют внешний вид, но по-прежнему считаются нечистыми:

  • function a() { return Date.now(); }
  • function b() { return window.globalMutableVar; }
  • function c() { return document.getElementById("myInput").value; }
  • function d() { return Math.random(); } (который, по общему признанию, меняет ГПСЧ, но не считается наблюдаемым)

Доступа к непостоянным нелокальным переменным достаточно, чтобы нарушить второе условие.

Я всегда считаю два условия чистоты взаимодополняющими:

  • оценка результата не должна влиять на побочное состояние
  • на результат оценки не должно влиять побочное состояние

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

Берги
источник
1
Спасибо, Берги. По какой-то причине я думал, что побочные эффекты включают чтение переменных вне локальной области, но я думаю, что это только побочный эффект, если он записывает такие внешние переменные.
Магнус
17
Если prompt("you choose")побочных эффектов нет, следует сделать шаг назад и уточнить значение побочных эффектов.
Holger
1
@Magnus Да, именно это и означает эффект . Я также постараюсь уточнить в своем ответе, я не ожидал такого большого внимания и хочу, чтобы ответ был достоин десятков голосов :-)
Берги
2
Насколько вам известно, Math.random () возвращает термодиод. Фактически не указано использование плохого ГСЧ.
Джошуа
1
Из двух условий я слышал, что первое называется «эффектами», а второе - «коэффектами». Оба являются «побочными эффектами» и нечистыми. f (coeffects, input) -> effects, output Коэффекты - это входные данные, возникающие в результате изменений в более широкой среде, на выходе - эффекты, которые изменяют более широкую среду. Например, Elm и Clojurescrips переделывают работу с этой моделью.
30

«Нормальный» способ сформулировать, что такое чистая функция , - это ссылочная прозрачность . Функция чиста, если она прозрачна по ссылкам .

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

Так, например, если C printfбыли ссылочно прозрачными, эти две программы должны иметь одинаковое значение:

printf("Hello");

и

5;

и все следующие программы должны иметь одно и то же значение:

5 + 5;

printf("Hello") + 5;

printf("Hello") + printf("Hello");

Потому как printf возвращает количество написанных символов, в данном случае 5.

С voidфункциями это становится еще более очевидным . Если у меня есть функция void foo, то

foo(bar, baz, quux);

должно быть таким же, как

;

Т.е. поскольку fooничего не возвращает, я должен иметь возможность заменить его ничем, не меняя смысла программы.

Таким образом, ясно, что ни одно из них, printfни fooявляется референциально прозрачным, и, следовательно, ни одно из них не является чистым. Фактически, voidфункция никогда не может быть ссылочно прозрачной, если только она не является нерабочей.

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

func fib(n):
    return memo[n] if memo.has_key?(n)
    return 1 if n <= 1
    return memo[n] = fib(n-1) + fib(n-2)

Мы можем проанализировать выражения, составляющие функцию, и легко прийти к выводу, что они не являются ссылочно прозрачными и, следовательно, не являются чистыми, поскольку они используют изменяемую структуру данных, а именно memoмассив. Тем не менее, мы можем рассмотреть функцию и можно увидеть , что она является референциально прозрачной и таким чистой. Иногда это называют внешней чистотой. , т. Е. Функцией, которая кажется чистой внешнему миру, но реализуется нечистой внутри.

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

Йорг В. Миттаг
источник
2
Эта примесь влияет на всю программу, если у вас есть параллелизм.
R .. GitHub НЕ ПОМОГАЕТ ICE
@R .. Можете ли вы придумать, каким образом параллелизм может сделать описанную функцию Фибоначчи внешне нечистой? Я не могу. Запись в memo[n]идемпотентна, а невозможность чтения из нее просто тратит циклы ЦП.
Brilliand
Я согласен с вами обоими. Примеси могут привести к проблемам с параллелизмом, но этого не происходит в данном конкретном случае.
Jörg W Mittag
@R .. Нетрудно представить версию с поддержкой параллелизма.
user253751 05
1
@Brilliand Например, memo[n] = ...может сначала создать словарную статью, а затем сохранить в ней значение. Это оставляет окно, в течение которого другой поток может увидеть неинициализированную запись.
user253751 05
12

Мне кажется, что второе описанное вами условие является более слабым ограничением, чем первое.

Позвольте мне привести пример. Предположим, у вас есть функция для добавления функции, которая также ведет журнал в консоли:

function addOneAndLog(x) {
  console.log(x);
  return x + 1;
}

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

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

Предположим, у нас есть функция, которая просто добавляет:

function addOne(x) {
  return x + 1;
}

Мы можем заменить addOne(5)на в 6любом месте нашей программы, и ничего не изменится.

Напротив, мы не можем заменить addOneAndLog(x)значение в 6любом месте нашей программы без изменения поведения, потому что первое выражение приводит к тому, что что-то записывается в консоль, а второе - нет.

Мы рассматриваем любое из этих дополнительных действий, которые addOneAndLog(x)выполняются помимо возврата вывода, как побочный эффект .

TheInnerLight
источник
«Мне кажется, что второе описанное вами условие является более слабым ограничением, чем первое». Нет, эти два условия логически независимы.
sleske 05
@sleske вы ошибаетесь. Я дал четкие определения терминов «чистый» и «побочный эффект». В рамках этих ограничений нет ничего, что могло бы вызвать функцию без побочных эффектов, кроме как возвращать тот же вывод для данного ввода. Однако я привел примеры, когда второе условие может быть выполнено без первого. Основная концепция для понимания понятия чистоты - ссылочная прозрачность.
TheInnerLight 05
Небольшая опечатка: функция без побочных эффектов не может делать ничего, кроме как возвращать тот же вывод для заданного ввода.
TheInnerLight 05
Как насчет того, чтобы вернуть текущее время? Это не имеет побочных эффектов, но возвращает другой результат для того же ввода. Или, в более общем смысле, любая функция, результат которой зависит не только от входных параметров, но и от (изменяемой) глобальной переменной.
sleske 05
2
Похоже, вы используете другое определение «побочного эффекта», чем обычно. Побочный эффект обычно определяется как «наблюдаемый эффект помимо возврата значения» или «наблюдаемое изменение состояния» - см., Например, Википедию , этот пост о программной инженерии.SE . Вы совершенно правы в том, что Date.now()он не является чистым / ссылочно прозрачным, но не потому, что он имеет побочные эффекты, а потому, что его результат зависит не только от его ввода.
sleske 05
7

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

Во всяком случае, все, о чем я могу думать.

пользователь3340459
источник
3
По моему мнению, эта «случайность извне системы» является побочным эффектом. Функции с таким поведением не являются «чистыми».
Джозеф М. Дион
2

Проблема с определениями FP в том, что они очень искусственные. Каждая оценка / вычисление имеет побочные эффекты для оценщика. Теоретически это правда. Отрицание этого показывает только то, что апологеты ФП игнорируют философию и логику: «оценка» означает изменение состояния некоторой интеллектуальной среды (машины, мозга и т. Д.). Таков характер процесса оценки. Без изменений - без «исчислений». Эффект может быть очень заметным: нагрев процессора или его выход из строя, выключение материнской платы в случае перегрева и так далее.

Когда вы говорите о ссылочной прозрачности, вы должны понимать, что информация о такой прозрачности доступна человеку как создателю всей системы и держателю семантической информации и может быть недоступна для компилятора. Например, функция может читать некоторый внешний ресурс, и в ее сигнатуре будет монада ввода-вывода, но она будет все время возвращать одно и то же значение (например, результат current_year > 0). Компилятор не знает, что функция всегда будет возвращать один и тот же результат, поэтому функция нечистая, но имеет ссылочно прозрачное свойство и может быть заменена Trueконстантой.

Итак, чтобы избежать такой неточности, мы должны различать математические функции и «функции» в языках программирования. Функции в Haskell всегда нечисты, и определение чистоты, связанной с ними, всегда очень условно: они работают на реальном оборудовании с реальными побочными эффектами и физическими свойствами, что неверно для математических функций. Это означает, что пример с функцией «printf» полностью неверен.

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

RandomB
источник
2

Если первое условие всегда верно, бывает ли когда-нибудь второе условие неверно?

да

Рассмотрим простой фрагмент кода ниже

public int Sum(int a, int b) {
    Random rnd = new Random();
    return rnd.Next(1, 10);
}

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

Общий эффект обеих точек №1 и №2, которые вы упомянули при объединении вместе, означает: в любой момент времени, если функция Sumс одинаковым i / p заменяется ее результатом в программе, общее значение программы не изменяется . Это не что иное, как ссылочная прозрачность .

rahulaga_dev
источник
Но в этом случае первое условие не проверяется: запись в консоль считается побочным эффектом, так как меняет состояние самой машины.
Правая нога
@Rightleg thx, что указали на это. Как-то я совершенно неправильно понял OP. исправленный ответ.
rahulaga_dev 05
2
Разве это не меняет состояние генератора случайных чисел?
Эрик
1
Генерация случайного числа сама по себе является побочным эффектом, если только состояние генератора случайных чисел не указано явно, что заставит функцию удовлетворять условию 2.
TheInnerLight 05
1
rndне экранирует функцию, поэтому тот факт, что ее состояние изменяется, не имеет значения для чистоты функции, но тот факт, что Randomконструктор использует текущее время в качестве начального значения, означает, что есть «входы», отличные от aи b.
Sneftel 05