Почему функция с бесполезным изолированным `static` считается нечистой?

В статье Википедии о чистой функции есть пример такой нечистой функции:

void f() {
  static int x = 0;
  ++x;
}

С замечанием «из-за мутации локальной статической переменной».

Интересно, почему это нечисто? Это от типа единицы к типу единицы, поэтому он всегда возвращает один и тот же результат для одного и того же ввода. И у него нет побочных эффектов, потому что, несмотря на то, что у него есть переменная static int, его не может наблюдать ни одна другая функция, кроме этой f(), поэтому нет наблюдаемой мутации глобального состояния, которую могли бы использовать другие функции.

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

Итак, почему эта бесполезная изолированная static int делает функцию нечистой?


person toriningen    schedule 05.03.2020    source источник
comment
Например: достаточно частый вызов f приведет к целочисленному переполнению и, следовательно, к неопределенному поведению, если я не ошибаюсь.   -  person churill    schedule 05.03.2020
comment
Если x действительно достаточно изолирован, то f можно считать чистым. Но обычно это означает, что вам на самом деле не нужна переменная.   -  person François Andrieux    schedule 05.03.2020
comment
@Mat Это действительно глобальная переменная с областью действия, ограниченной только другими экземплярами f(), что делает ее недоступной для наблюдения какой-либо другой функцией (поэтому они не могут изменить свое поведение на основе значения x). Если этого никто не видит, значит, этого не существует. Если это плохо, то почему локальные переменные (выделенные в стеке) не нарушают чистоты? Локальные переменные также являются глобальными переменными с ограниченной областью действия в некотором смысле.   -  person toriningen    schedule 05.03.2020
comment
Я думаю, что этот вопрос можно лучше сформулировать так: почему полезно рассматривать этот вырожденный случай нечистой функции как нечистую? Поскольку мы не можем возражать против предоставленного определения (функция изменяет локальную статическую переменную. Это определение. Конец обсуждения).   -  person JohnFilleau    schedule 05.03.2020
comment
@churill, даже если это поведение undefined, естественно, что любая функция может иногда взорваться из-за проблемы с остановкой (и в результате вернуть ). Например, для какой-то чистой функции может быть выделен стек настолько большой, что он испортит стек другой функции, не нарушая его чистоты.   -  person toriningen    schedule 05.03.2020
comment
Пример урезан, чтобы выделить только один аспект: изменение локальной статической переменной. Небрежно говоря, чисто означает, что функция делает то же самое для того же ввода   -  person 463035818_is_not_a_number    schedule 05.03.2020
comment
На первый взгляд, может быть, это определено таким образом, потому что 1. сделать общую и полезную классификацию вещей сложно и 2. просто проще вызвать любую функцию, которая, как правило, мутирует локальную статическую нечистоту, и обрабатывать странные исключения, где эта локальная переменная не используется ни для чего в качестве исключения, которое может потребовать дополнительного рассмотрения   -  person JohnFilleau    schedule 05.03.2020
comment
Я предполагаю, что если вы пишете статью на эту тему и хотите использовать приведенную выше функцию в качестве примера чистой функции, потому что ее изменение внутреннего состояния полностью отделено от всего остального логического потока программы, просто убедитесь, что это конкретное определение. Я уверен, что многие ученые кивнули бы в знак согласия, в то время как другие бросили бы моноколь / брызги шампанского / испортили аскот.   -  person JohnFilleau    schedule 05.03.2020
comment
Непонятно, для какой цели здесь служит x, он никогда не раскрывается и не доступен для другого кода. Если каким-то образом это было доступно, у вас были бы проблемы с запуском этого многопоточного, x++ не атомарного, что означает, что он, возможно, далек от чисто функционального. Одним из способов определения чистых функций является то, что они не обмениваются данными между вызовами.   -  person tadman    schedule 05.03.2020
comment
@Джон, другими словами, статья в Википедии просто неверна с этим примером из-за чрезмерного упрощения?   -  person toriningen    schedule 05.03.2020
comment
Вы также можете смешивать процесс подготовки вызова функции, например выделения памяти в стеке, если это необходимо, с выполнением вызова функции. Каждая программа, чисто функциональная или нет, имеет какое-то внутреннее состояние, иначе было бы невозможно что-либо вычислить. Большинство языков функционального программирования не считают стек чем-то, чем нужно заниматься, это артефакт того, как выполняется код, и ограничение архитектуры фон Неймана. Возможно, удастся построить функциональный компьютер без этого ограничения.   -  person tadman    schedule 05.03.2020
comment
@tadman, если бы он был каким-то образом доступен для любой функции, кроме рассматриваемой f(), то f() не был бы чистым со 100% уверенностью, потому что другие функции могут теоретически изменить свое поведение на основе значения x, даже если они не. Но так как он недоступен, и неважно, что f() делает с x (вычисление 1000*x-й цифры числа π может быть многопоточным?), пока его нигде не видно, он чистый, не так ли?   -  person toriningen    schedule 05.03.2020
comment
Если x только манипулируется и никогда не используется, то он является кандидатом на удаление достаточно умным оптимизирующим компилятором. Вопрос здесь в том, учитывается ли когда-либо x в каких-либо других вычислениях? а если нет то x можно смело удалять. Если его можно удалить, то эту функцию можно сделать чисто функциональной, но для этого потребуется исключить этот термин.   -  person tadman    schedule 05.03.2020
comment
что ж, любой умный компилятор также полностью удалит f(), потому что любой чистый _ -> unit неотличим от простого константного unit.   -  person toriningen    schedule 05.03.2020
comment
Вы можете поэкспериментировать с компиляторами C++, такими как clang, чтобы увидеть, что они делают с включенной полной оптимизацией, а также что другие функциональные языки, такие как Haskell или Erlang, делают с той же функцией.   -  person tadman    schedule 05.03.2020
comment
Что касается подготовки и выполнения - назначение локальных переменных (что обычно считается сохранением чистоты в императивных языках) изменяет содержимое глобальной памяти (доступное нечистыми средствами), так что это дырявая абстракция. так или иначе.   -  person toriningen    schedule 05.03.2020
comment
Мне было бы любопытно узнать, как изменится всеобщее представление о чистоте этой функции, если ее заменить атомарной переменной с определенным переполнением.   -  person JohnFilleau    schedule 05.03.2020
comment
@Джон Я тоже. Подписать его было, вероятно, невольным отвлечением. Неряшливый пример Wiki, я думаю.   -  person Ted Lyngmo    schedule 05.03.2020
comment
Для дополнительного чтения интересен раздел обсуждения статьи в Википедии. обсуждение   -  person JohnFilleau    schedule 05.03.2020
comment
О, Боже. Согласно gcc, чистая функция может читать глобальные переменные. Разве это не позволяет функции иметь разные выходные данные для одного и того же ввода? Или мы рассматриваем глобальное как вход, я полагаю.   -  person JohnFilleau    schedule 05.03.2020
comment
@Джон, я бы не стал принимать во внимание понятие чистых функций gcc ... Я думаю, им пришлось сделать это, чтобы сделать глобально настраиваемые функции, такие как sin(), вроде бы чистыми. Нереференциально прозрачные функции нельзя считать чистыми.   -  person toriningen    schedule 05.03.2020
comment
@toriningen имеет значение, как компилятор использует чистоту, потому что я думал, что вопрос в том, правильно ли определение в статье в Википедии или нет. Если статья в Википедии является правдой, то ваша включенная функция нечиста. Если определение статьи в Википедии подлежит обсуждению, тогда возникает вопрос: какое определение является лучшим для чистой функции? и один из способов понять определение слова — посмотреть, как оно используется. Думаю в итоге получится, Эта функция академически чистая, но практически нечистая   -  person JohnFilleau    schedule 05.03.2020


Ответы (2)


Результат чистой функции полностью определяется ее входными аргументами. Здесь под результатом понимается не только возвращаемое значение, но и эффект с точки зрения виртуальной машины, определенный стандартом C/C++. Другими словами, если функция иногда демонстрирует неопределенное поведение с одними и теми же входными аргументами, ее нельзя считать чистой (поскольку поведение отличается от одного вызова к другому с одинаковыми входными аргументами). вход).

В конкретном случае со статической локальной переменной эта переменная может стать источником гонки данных, если f вызывается одновременно в нескольких потоках. Гонка данных означает неопределенное поведение. Другой возможный источник UB — переполнение целого числа со знаком, которое может в конечном итоге произойти.

person Andrey Semashev    schedule 05.03.2020
comment
Я не понимаю, как происходит гонка данных. Любые изменения в x не имеют значения, независимо от того, было ли вычисление действительным (x было увеличено) или нет (произошло UB, x было увеличено дважды и т. д.), потому что f() в своей форме не имеет возможности сообщить содержимое x вызывающей стороне. , ни один из вызывающих абонентов не может прочитать x каким-либо образом в обход f(). И для чистых функций разрешено вылетать или никогда не возвращаться, потому что существует допустимый термин _ -> ⊥, подобный этому f = f (). - person toriningen; 05.03.2020
comment
› Любое изменение x не имеет значения -- имеет значение. Гонка данных означает, что поведение действительно не определено. Реализации разрешено сбой или форматирование вашего жесткого диска или что-то еще. На практике гонка данных во время приращения может создать перехватывающее представление int, которое будет сигнализировать и аварийно завершать работу при следующем вызове f. - person Andrey Semashev; 05.03.2020
comment
Я не согласен. Если у меня есть функция int f(i) { return i + 1; }, i + 1 может переполниться. Разве это не чисто? Я бы считал эту функцию чистой. - person Ayxan Haqverdili; 05.03.2020
comment
› И чисто функциям разрешено аварийно завершать работу или никогда не возвращаться -- Верно, но только последовательно для одного и того же ввода. т.е. если он дает сбой для какого-то ввода, он должен падать каждый раз для одного и того же ввода, однако и всякий раз, когда вы вызываете функцию. - person Andrey Semashev; 05.03.2020
comment
@Ayxan поведение (или неопределенное поведение) зависит от ввода, поэтому оно чистое. В случае OP это зависит от того, как часто вы вызываете функцию (для случая переполнения). - person Kevin; 05.03.2020
comment
@Ayxan Да, ваш пример - чистая функция. Функция в исходном вопросе — нет, потому что она ведет себя по-разному от одного вызова к другому для одного и того же ввода. - person Andrey Semashev; 05.03.2020
comment
Значит, если бы не было УБ (потому что было бы static unsigned int x), то оно становится чистым? - person toriningen; 05.03.2020
comment
@AndreySemashev Я обновил статью в Википедии и страницу обсуждения, используя знания, полученные из вашего ответа. - person toriningen; 05.03.2020
comment
› То есть, если бы не было УБ (потому что было бы static unsigned int x), то оно стало бы чистым? -- Если нет UB (что означает не только unsigned int, но и atomic или mutex для синхронизации параллельного доступа), то да. То есть, пока x не влияет на вывод функции, в этот момент f перестает быть чистым. Это означает, что x довольно бесполезен. - person Andrey Semashev; 06.03.2020
comment
@toriningen Если ваше редактирование является примером в en.wikipedia.org/wiki/Pure_function#Pure_functions и его обсуждение, то правка неправильная. Эта функция не чистая, как объясняется в моем ответе. - person Andrey Semashev; 06.03.2020

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

не имеет наблюдаемого побочного эффекта

Это важно, потому что одна из вещей, которые мы можем сделать с чистой функцией (в отличие от нечистой функции), — это мемоизация (по ссылке выше) или кэширование ввода/вывода. Чистые функции также проверяемы, разумны и самодокументируемы.

Я предполагаю, что запоминание имеет значение для компилятора, поэтому вопрос о том, является ли функция "чистой" может считаться эквивалентным вопросу о том, может ли компилятор запомнить функцию. Кажется, что концепция локальной переменной static, которую никакая другая часть кода не затрагивает, — это просто плохой код, и компилятор должен выдать предупреждение об этом. Но должен ли компилятор оптимизировать его? И должен ли компилятор попытаться выяснить, действительно ли любая заданная static локальная переменная не имеет побочных эффектов?

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

Так что с точки зрения компилятора это нечисто.

А как насчет других свойств чистой функции?

проверяемый, разумный и самодокументируемый

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

Является ли код разумным? Конечно нет. Хотя я не уверен, насколько это важно. Он ничего не делает. Это затрудняет понимание. («Почему Боб написал функцию таким образом? Это ситуация Magic/More Magic?»).

Является ли код самодокументируемым? Опять же, я бы сказал, что нет. Но опять же, это вырожденный пример кода.

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

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

«В заключение, мне все равно, чистый он или нет».

Позвольте мне перефразировать мой предыдущий (выше) вывод.

Я склонен просто сканировать функцию на предмет любой мутации статических переменных и заканчивать работу. Бум, уже не чисто.

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

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

person JohnFilleau    schedule 05.03.2020
comment
Мне все равно, чистый он или нет. Ну, да. Что касается описанных вами свойств (таких как самодокументируемость и разумность), то они причудливы, но на самом деле не связаны с чистотой, потому что вы не можете определить их формально. Однако такие вещи, как ссылочная прозрачность, могут быть. - person toriningen; 05.03.2020
comment
@toriningen мы изо дня в день имеем дело с вещами, которые формально не определены. Такова природа жизни. Если вы хотите написать статью, в которой имеет значение это очень строгое определение чистоты, назовите эту функцию чистой. Если вы хотите использовать определение pure для выполнения полезной работы, вам, вероятно, нужно немного расслабиться и просто смириться с вызовом этой функции impure ради того, чтобы идти домой в 5 часов. - person JohnFilleau; 05.03.2020
comment
я бы скорее сказал наоборот. Термин чистая функция определяется таким образом, чтобы он был практичным. Если бы определение гласило: Well pure означает одинаковый вывод для одного и того же ввода, это было бы бесполезно, но наличие точного определения этого термина делает его полезным. - person 463035818_is_not_a_number; 06.03.2020