Чому використання оператора або циклів призначення не відволікається у функціональному програмуванні?


9

Якщо моя функція відповідає нижчим двом вимогам, я вважаю, що функція Sum повертає підсумки елементів у списку, де елемент оцінюється як істинне для заданої умови, можна вважати чистою функцією, чи не так?

1) Для заданого набору i / p повертається той же o / p незалежно від часу, коли викликається функція

2) Це не має жодних побічних ефектів

public int Sum(Func<int,bool> predicate, IEnumerable<int> numbers){
    int result = 0;
    foreach(var item in numbers)
        if(predicate(item)) result += item;
    return result;
}

Приклад: Sum(x=>x%2==0, new List<int> {1,2,3,4,5...100});

Причина, що я задаю це питання, тому що я бачу майже кожного, де люди радять уникати операторів і циклів призначення, оскільки це обов'язково стиль програмування. То що може піти не так у наведеному вище прикладі, який використовує оператори циклів та призначення в контексті програмування функцій?


1
Він не має жодних побічних ефектів - він має побічний ефект, коли itemзмінна мутується в циклі.
Фабіо

@Fabio ok. Але чи можете ви детальніше розглянути сферу побічного ефекту?
rahulaga_dev

Відповіді:


16

У чому полягає значення функціонального програмування?

Функціональне програмування за принципом декларативне . Ви кажете, що ваш результат, а не як його обчислити.

Давайте розглянемо дійсно функціональну реалізацію вашого фрагмента. У Haskell це було б:

predsum pred numbers = sum (filter pred numbers)

Чи зрозуміло, який результат? Зовсім так, це сума чисел, що відповідають присудку. Як це обчислюється? Мені все одно, запитайте у компілятора.

Ви, можливо, можете сказати, що використовувати sumі filterхитрість, і це не враховується. Нехай тоді реалізують це без цих помічників (хоча найкращим способом було б спочатку їх реалізувати).

Рішення "Функціональне програмування 101", яке не використовується, sumє з рекурсією:

sum pred list = 
    case list of
        [] -> 0
        h:t -> if pred h then h + sum pred t
                         else sum pred t

Досі зрозуміло, що є результатом в частині виклику однієї функції. Він є або 0, або recursive call + h or 0залежно від цього pred h. І все-таки досить прямо, навіть якщо кінцевий результат не відразу очевидний (хоча, маючи трохи практики, це дійсно читається так само, як forцикл).

Порівняйте це з вашою версією:

public int Sum(Func<int,bool> predicate, IEnumerable<int> numbers){
    int result = 0;
    foreach(var item in numbers)
        if (predicate(item)) result += item;
    return result;
}

Який результат? О, я не бачу: одне returnзаяву, не дивує тут: return result.

Але що таке result? int result = 0? Не здається правильним. Ви робите щось пізніше з цим 0. Гаразд, ви додасте itemдо нього s. І так далі.

Звичайно, для більшості програмістів це досить очевидно, що трапляється в такому простому функціонері, але додайте додаткову returnзаяву або так, і це раптом стає важче відстежити. Весь код про те , як і що залишається читачеві розібратися - це, очевидно, дуже імперативний стиль .

Отже, чи не змінні змінні та петлі?

Ні.

Є багато речей, які набагато простіше пояснити їм, і багато алгоритмів, які потребують швидкого зміни стану. Але змінні по суті є імперативними, пояснюючи, як замість чого , і не даючи передбачень того, яке їх значення може бути через кілька рядків пізніше або після декількох ітерацій циклу. Петлі, як правило, потребують стану, щоб мати сенс, і тому вони також є суттєво необхідними.

Змінні та петлі - це просто не функціональне програмування.

Підсумок

Сучасне функціональне програмування - це трохи більше стилю та корисного способу мислення, ніж парадигма. Сильна перевага для чистих функцій є в цьому режимі мислення, але насправді це лише невелика частина.

Найбільш поширені мови дозволяють використовувати деякі функціональні конструкції. Наприклад, у Python ви можете вибрати:

result = 0
for num in numbers:
    if pred(result):
        result += num
return result

або

return sum(filter(pred, numbers))

або

return sum(n for n in numbers if pred(n))

Ці функціональні вирази чудово підходять для таких проблем і просто роблять код коротшим (а коротше - добре ). Не варто бездумно замінювати імперативний код на них, але коли вони підходять, вони майже завжди є кращим вибором.


THX для приємного пояснення !!
rahulaga_dev

1
@RahulAgarwal Ця відповідь може здатися цікавою, вона гарно фіксує дотичну концепцію встановлення істин проти опису кроків. Мені також подобається словосполучення "декларативні мови містять побічні ефекти, а імперативні мови цього не роблять" - зазвичай функціональні програми мають чіткі і дуже помітні розрізи між стаціонарним кодом, що стосується зовнішнього світу (або виконує якийсь оптимізований алгоритм), і чисто функціональним кодом.
Frax

1
@Frax: спасибі !! Я загляну в це. Також нещодавно натрапили на розмови Rich Hickey про значення цінностей Це дійсно геніально. Я думаю, що одне правило - "працювати зі значеннями та виразом, а не працювати з чимось утримуваним значенням і може змінитися"
rahulaga_dev

1
@Frax: Також справедливо сказати, що FP - це абстракція над імперативним програмуванням - адже в кінцевому підсумку хтось повинен доручити машині "як це зробити", правда? Якщо так, то чи не обов'язкове програмування має більш низький рівень контролю в порівнянні з FP?
rahulaga_dev

1
@Frax: Я погодився би з Рахулем, що імператив є нижчим рівнем у тому сенсі, що він ближче до основної машини. Якщо апаратне забезпечення може зробити копії даних безкоштовно, нам не знадобляться руйнівні оновлення для підвищення ефективності. У цьому сенсі імперативна парадигма наближається до металу.
Джорджіо

9

Застосування мутаційного стану, як правило, не дає можливості функціонувати програмі. Петлі відключаються як наслідок, оскільки петлі корисні лише в поєднанні зі змінним станом.

Функція в цілому чиста, що чудово, але парадигма функціонального програмування застосовується не лише на рівні цілих функцій. Ви також хочете уникнути змінного стану також на локальному рівні, всередині функцій. І міркування в основному ті ж: Уникнення змінних станів полегшує розуміння коду та запобігає появі певних помилок.

У вашому випадку ви можете написати, numbers.Where(predicate).Sum()що явно набагато простіше. А простіше означає менше помилок.


Дякую !! Я думаю, що мені не вистачало вражаючої лінії - але парадигма функціонального програмування застосовується не лише на рівні цілих функцій, але тепер мені також цікаво, як візуалізувати цю межу. В основному, з точки зору споживача, це чиста функція, але розробник, який фактично написав цю функцію, не дотримувався чистих вказівок щодо функцій? плутати :(
rahulaga_dev

@RahulAgarwal: Яка межа?
ЖакБ

Я збентежений у сенсі, якщо парадигма програмування вважає FP з точки зору споживача функцій? Bcoz, якщо я дивлюся на реалізацію впровадження Whereв numbers.Where(predicate).Sum()- це використовуєforeach цикл.
rahulaga_dev

3
@RahulAgarwal: Як споживач функції, вам не дуже важливо, чи функція або модуль внутрішньо використовує стан, що змінюється, доки він зовні чистий.
ЖакБ

7

Хоча ви правильні, що з точки зору зовнішнього спостерігача ваша Sumфункція є чистою, внутрішня реалізація явно не є чистою - у вас зберігається стан, в resultякому ви неодноразово мутуєте. Однією з причин уникати змінного стану є те, що він створює більше когнітивного навантаження на програміста, що, в свою чергу, призводить до появи більше помилок [потрібне цитування] .

Незважаючи на те, що в такому простому прикладі кількість зберігається змінних станів, ймовірно, досить мала, що серйозних проблем це не спричинить, загальний принцип все ще застосовується. Такий приклад іграшки, як, Sumмабуть, не найкращий спосіб продемонструвати перевагу функціонального програмування над імперативом - спробуйте зробити щось із великою кількістю змінних станів, і переваги можуть стати зрозумілішими.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.