Что означает, что нечистые функции нарушают композицию?
Может ли кто-нибудь привести пример, который объясняет, что это означает на практике, когда люди говорят, что нечистые функции нарушают способность к функциональности в функциональных языках?
Я хотел бы увидеть пример композитоспособности, а затем увидеть тот же пример, предполагающий нечистые функции и то, как нечеткость нарушила композицию.
Ответы
Ответ 1
Некоторые примеры, когда измененное состояние укусило меня в прошлом:
-
Я пишу функцию, чтобы очистить некоторую информацию от беспорядка текста. Он использует простое регулярное выражение, чтобы найти нужное место в беспорядке и захватить некоторые байты. Он перестает работать, потому что другая часть моей программы включала чувствительность к регистру в библиотеке регулярных выражений; или включил "волшебный" режим, который изменяет способ анализа регулярных выражений; или любой из дюжины других ручек, которые я забыл, были доступны, когда я написал вызов регулярному выражению.
Это не проблема в чистых языках, потому что параметры регулярного выражения отображаются как явные аргументы для соответствующей функции.
-
У меня есть два потока, которые хотят сделать некоторые вычисления в моем синтаксическом дереве. Я иду за этим, не задумываясь об этом. Поскольку оба вычисления включают в себя переписывающие указатели в дереве, я заканчиваю segfault, когда я следую указателю, который был хорош до этого, но стал устаревшим из-за изменений, сделанных другим потоком.
Это не проблема в чистых языках, где дерево неизменно; два потока возвращают деревья, которые живут в разных частях кучи, и оба получают возможность увидеть первоначальный оригинал без вмешательства другого.
-
У меня лично нет опыта с этим, но я слышал, как другие программисты обманывали его: в основном каждая программа, использующая OpenGL. Управление конечным автоматом OpenGL - это кошмар. Каждый вызов делает что-то глупое, если вы немного ошибаетесь в какой-либо части состояния.
Трудно сказать, как это выглядело бы в чистой обстановке, так как не так много широко используемых библиотек чистой графики. Для третьей стороны можно посмотреть fieldtrip
, а на 2-й стороне, возможно, diagrams
, как на Haskell-land. В каждом из описаний сцены есть композиционные в том смысле, что можно легко объединить две маленькие сцены в более крупные с комбинаторами, такими как "положить эту сцену влево от этой", "наложить эти две сцены", "показать эту сцену после этого", и т.д., и бэкэнд гарантирует, что он будет разбивать базовое состояние графической библиотеки между вызовами, которые отображают две сцены.
Общий поток в нечистых сценариях, описанных выше, заключается в том, что нельзя смотреть на кусок кода и выяснить, что он делает локально. Необходимо иметь глобальное понимание всей базы кода, чтобы убедиться, что они понимают, что будет делать кусок кода. Это основной смысл композиции: можно составить небольшие куски кода и понять, что они делают; и когда они помещаются в большую программу, они все равно будут делать то же самое.
Ответ 2
Я не думаю, что вы собираетесь "видеть тот же пример, предполагающий нечистые функции и то, как нечистота нарушила композицию". Любая ситуация, когда побочные эффекты являются проблемой для способности к слиянию, - это та, которая не будет возникать с чистыми функциями.
Но вот пример того, что люди имеют в виду, когда говорят, что "нечистые функции ломают составность":
Скажем, у вас есть POS-система, что-то вроде этого (делайте вид, что это С++ или что-то еще):
class Sale {
private:
double sub_total;
double tax;
double total;
string state; // "OK", "TX", "AZ"
public:
void calculateSalesTax() {
if (state == string("OK")) {
tax = sub_total * 0.07;
} else if (state == string("AZ")) {
tax = sub_total * 0.056;
} else if (state == string("TX")) {
tax = sub_total * 0.0625;
} // etc.
total = sub_total + tax;
}
void printReceipt() {
calculateSalesTax(); // Make sure total is correct
// Stuff
cout << "Sub-total: " << sub_total << endl;
cout << "Tax: " << tax << endl;
cout << "Total: " << total << endl;
}
Теперь вам нужно добавить поддержку для Oregon (без налога с продаж). Просто добавьте блок:
else if (state == string("OR")) {
tax = 0;
}
до calculateSalesTax
. Но предположим, что кто-то решает получить "умный" и сказать
else if (state == string("OR")) {
return; // Nothing to do!
}
вместо этого. Теперь total
больше не рассчитывается! Поскольку выходы функции calculateSalesTax
не все понятны, программист произвел изменение, которое не дает всех правильных значений.
Возвращаясь к Haskell, с чистыми функциями вышеуказанный дизайн просто не работает; вместо этого вы должны сказать что-то вроде
calculateSalesTax :: String -> Double -> (Double, Double) -- (sales tax, total)
calculateSalesTax state sub_total = (tax, sub_total + tax) where
tax
| state == "OK" = sub_total * 0.07
| state == "AZ" = sub_total * 0.056
| state == "TX" = sub_total * 0.0625
-- etc.
printReceipt state sub_total = do
let (tax, total) = calculateSalesTax state sub_total
-- Do stuff
putStrLn $ "Sub-total: " ++ show sub_total
putStrLn $ "Tax: " ++ show tax
putStrLn $ "Total: " ++ show total
Теперь очевидно, что Oregon нужно добавить добавлением строки
| state == "OR" = 0
к вычислению tax
. Ошибка устранена, так как входы и выходы функции все явные.
Ответ 3
Один аспект заключается в том, что чистота позволяет ленивую оценку и ленивая оценка позволяет некоторые формы композиции вы не можете делать на строго оцененном языке.
Например, в Haskell вы можете создавать конвейеры из map
и filter
, которые тратят только O (1) память, и у вас больше свободы для записи функций "control-flow", таких как ваш собственный ifThenElse или материал на Control.Monad.
Ответ 4
Ответ на самом деле довольно прост: если у вас есть нечистые функции, то есть функции с побочными эффектами, побочные эффекты могут мешать друг другу. Элементарный пример - это функция, которая хранит что-то во внешней переменной во время ее выполнения. Две функции, которые используют одну и ту же переменную, не будут составлять - только один результат будет сохранен. Этот пример может показаться тривиальным, но в сложной системе с несколькими нечистыми функциями столкновения при доступе к различным ресурсам могут быть очень трудными для отслеживания.
Классический пример - защита изменяемых (или иначе исключительных) ресурсов в многопоточной среде. Единая функция доступа к ресурсу работает без проблем. Но две такие функции, запущенные в разных потоках, не являются - они не сочиняют.
Таким образом, мы добавляем блокировку для каждого ресурса и приобретаем/освобождаем блокировку по мере необходимости для синхронизации операций. Но опять же, функции не сочиняют. Запуск функций, выполняющих только одну блокировку параллельно, отлично работает, но если мы начнем комбинировать наши функции в более сложные, и каждый поток может получить несколько блокировок, мы можем получить тупики (один поток получает Lock1, а затем запрашивает Lock2, а другой получает Lock2, а затем запрашивает Lock1).
Поэтому мы требуем, чтобы все потоки приобретали блокировки в определенном порядке, чтобы предотвратить взаимоблокировки. Теперь структура без взаимоблокировки, но, к сожалению, снова функции не создаются по другой причине: если f1
принимает Lock2
и f2
, то требуется выход f1
, чтобы решить, какую блокировку взять, и f2
запрашивает Lock1
на основе ввода, инвариант порядка нарушается, хотя f1
и f2
отдельно удовлетворяют этому....
Композитным решением этой проблемы является транзакционная память программного обеспечения или просто STM
. Каждое такое вычисление выполняется в транзакции и перезапускается, если его доступ к совместно используемому изменяемому состоянию мешает другому вычислению. И здесь строго требуется, чтобы вычисления были чистыми - вычисления могут быть прерваны и перезапущены в любое время, поэтому любые их побочные эффекты будут выполняться только частично и/или несколько раз.