2015-04-16 3 views
11

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

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

+1

Вот один из моих любимых сообщений о том, как чистота и лень позволяют создавать композицию: http://apfelmus.nfshost.com/articles/quicksearch.html – luqui

ответ

1

Одним из аспектов является то, что чистота позволяет ленивую оценку и lazy evaluation enables some forms of composition you can't do in a strictly evaluated language.

Например, в Haskell можно создавать трубопроводы map и filter что потратит O (1) память, и у вас есть больше свободы, чтобы написать «управление потоком» функцию, такие, как ваш собственный ifThenElse или материал на Control.Monad ,

3

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

Но вот пример того, что означает, что люди, когда они говорят «не чистые функции сломать компонуемости»:

Допустим, у вас есть система POS, что-то вроде этого (изобразите это C++ или что-то):

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 

Теперь очевидно, что штат Орегон должен быть добавлен, добавив его

| state == "OR" = 0 

для расчета tax. Ошибка предотвращается, так как входы и выходы в функцию все явные.

+0

Непонятно, что проблема в этом примере - это мутация, а не ранняя return - конструкция, которая долгое время подвергалась критике, с первых дней процедурного императивного программирования. В качестве примера дьявола, например, рассмотрим смешанный функциональный/императивный язык, такой как Schema или ML, где нет ограничений на мутацию, но язык не предлагает ранний «возврат» от функции. Тогда гораздо сложнее написать ошибку, как в примере. –

+0

Даже если 'return' является проблемой, я думаю, что это все еще отвечает на вопрос. 'return' - нечистая функция, эффект. – luqui

10

Некоторых примеров, где изменяемое состояние укусило меня в прошлом:

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

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

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

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

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

    Трудно сказать, как это будет выглядеть в чистой обстановке, поскольку не так много широко используемых библиотек чистой графики. Для третьей стороны можно было посмотреть fieldtrip, а на 2-й стороне, возможно, diagrams, оба из Haskell-land. В каждом из описаний сцены есть композиционные в том смысле, что можно легко объединить две маленькие сцены в более крупные с комбинаторами, такими как «положить эту сцену влево от этой», «наложить эти две сцены», «показать эту сцену после этого», , и т. д., и бэкэнд обеспечивает отображение состояния базовой графической библиотеки между вызовами, которые отображают две сцены.

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

0

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

Классический пример защиты изменяемых (или иных) ресурсов в многопоточной среде. Единая функция доступа к ресурсу работает без проблем. Но две такие функции, работающие в разных потоках) не делают - они не сочиняют.

Таким образом, мы добавляем блокировку для каждого ресурса и приобретаем/освобождаем блокировку по мере необходимости для синхронизации операций. Но опять же, функции не сочиняют. Запуск функций, выполняющих только одну блокировку параллельно, отлично работает, но если мы начнем комбинировать наши функции с более сложными, и каждый поток может получить несколько блокировок, мы можем получить deadlocks (один поток получает Lock1, а затем запрашивает Lock2, а другой получает Lock2, а затем запросит Lock1).

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

компонуемо решением этой проблемы является Software transactional memory или просто STM. Каждое такое вычисление выполняется в транзакции и перезапускается, если его доступ к совместно используемому изменяемому состоянию мешает другому вычислению. И здесь строго требуется, чтобы вычисления были чистыми - вычисления могут быть прерваны и перезапущены в любое время, поэтому любые их побочные эффекты будут выполняться только частично и/или несколько раз.

Смежные вопросы