Что означает, что нечистые функции нарушают композицию?

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

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

Ответ 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. Каждое такое вычисление выполняется в транзакции и перезапускается, если его доступ к совместно используемому изменяемому состоянию мешает другому вычислению. И здесь строго требуется, чтобы вычисления были чистыми - вычисления могут быть прерваны и перезапущены в любое время, поэтому любые их побочные эффекты будут выполняться только частично и/или несколько раз.