По-справжньому розуміючи різницю між процедурним та функціональним


114

Мені справді важко зрозуміти різницю між парадигмами процедурної та функціональної програми.

Ось перші два абзаци із статті Вікіпедії про функціональне програмування :

В інформатиці функціональне програмування - це парадигма програмування, яка розглядає обчислення як оцінку математичних функцій та уникає даних про стан та змінні. Він підкреслює застосування функцій на відміну від імперативного стилю програмування, який підкреслює зміни у стані. Функціональне програмування має своє коріння в обчисленні лямбда, формальній системі, розробленій в 1930-х роках для дослідження визначення функцій, застосування функції та рекурсії. Багато функціональних мов програмування можна розглядати як опрацьовування лямбда-числення.

На практиці різниця між математичною функцією та поняттям "функція", що використовується в імперативному програмуванні, полягає в тому, що імперативні функції можуть мати побічні ефекти, змінюючи значення стану програми. Через це їм не вистачає еталонної прозорості, тобто один і той же мовний вираз може призводити до різних значень у різний час залежно від стану програми, що виконується. І навпаки, у функціональному коді вихідне значення функції залежить лише від аргументів, які вводяться у функцію, тому виклик функції fдвічі з тим самим значенням для аргументу xдасть той самий результатf(x)обидва рази. Усунення побічних ефектів може значно полегшити розуміння та прогнозування поведінки програми, що є однією з ключових мотивацій розвитку функціонального програмування.

У пункті 2, де сказано

І навпаки, у функціональному коді вихідне значення функції залежить лише від аргументів, які вводяться у функцію, тому виклик функції fдвічі з однаковим значенням для аргументу xдасть однаковий результат f(x)обох разів.

Це не той самий випадок, що стосується процесуального програмування?

На що слід звернути увагу у процедурних та функціональних, які виділяються?


1
Посилання "Абаяльний пітон: функціональне програмування на Python" з Абафея було розірвано. Ось хороший набір посилань: ibm.com/developerworks/linux/library/l-prog/index.html ibm.com/developerworks/linux/library/l-prog2/index.html
Кріс Кокнат

Іншим аспектом цього є називання. Напр. у JavaScript та Common Lisp ми використовуємо термін функція, навіть якщо їм дозволені побічні ефекти, а в Scheme це ж послідовно називається procedureere. Чиста функція CL може бути записана як чиста функціональна процедура Схеми. Практично у всіх книгах про схему використовується термін процедура, оскільки це tyerm, що використовується в стандарті, і це не має нічого спільного з тим, щоб бути процедурним або функціональним.
Сільвестер

Відповіді:


276

Функціональне програмування

Функціональне програмування стосується можливості трактувати функції як значення.

Розглянемо аналогію з "регулярними" значеннями. Ми можемо взяти два цілих значення і об'єднати їх за допомогою +оператора для отримання нового цілого числа. Або ми можемо помножити ціле число на число з плаваючою комою, щоб отримати число з плаваючою точкою.

У функціональному програмуванні ми можемо поєднати два значення функції для отримання нового значення функції за допомогою операторів, таких як compose або lift . Або ми можемо поєднати значення функції та значення даних, щоб створити нове значення даних, використовуючи такі оператори, як map або fold .

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

Процедурне програмування

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

Контрастність

Два стилі насправді не є протилежними - вони просто відрізняються один від одного. Є мови, які повністю охоплюють обидва стилі (наприклад, LISP). Наступний сценарій може дати відчуття деяких відмінностей у двох стилях. Давайте напишемо якийсь код для вимоги нісенітниці, де ми хочемо визначити, чи всі слова у списку мають непарну кількість символів. По-перше, процедурний стиль:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

Я сприйму це як даний факт, що цей приклад є зрозумілим. Тепер функціональний стиль:

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

Це визначення працює зсередини назовні:

  1. compose(odd, length)поєднує в собі oddі lengthфункції, щоб створити нову функцію, яка визначає, чи довжина рядка непарна.
  2. map(..., words)викликає цю нову функцію для кожного елемента в words, зрештою, повертає новий список булевих значень, кожен із яких вказує, чи має відповідне слово непарна кількість символів.
  3. apply(and, ...)застосовує оператор "і" до результуючого списку і -ing всіх булей разом, щоб отримати кінцевий результат.

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

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

Подальше читання

Це питання виникає багато ... дивіться, наприклад:

Лекція премії Джона Бекуса «Тюрінг» детально розглядає мотиви функціонального програмування:

Чи можна програмування звільнити від стилю фон Неймана?

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


Додаток - 2013 рік

Оглядачі зазначають, що популярні сучасні мови пропонують інші стилі програмування понад процедурні та функціональні. Такі мови часто пропонують один або декілька наступних стилів програмування:

  • запит (наприклад, розуміння списку, інтегрований мова)
  • потік даних (наприклад, неявна ітерація, масові операції)
  • об'єктно-орієнтовані (наприклад, інкапсульовані дані та методи)
  • орієнтований на мову (наприклад, синтаксис, макроси, специфічні для додатків)

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

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


1
Справді приємна відповідь, але чи можете ви трохи спростити код, наприклад: "функція allOdd (слова) {foreach (автоматичне слово у словах) {odd (length (word)? Return false:;} return true;}"
Dainius

Функціональний стиль там досить важко читати порівняно з "функціональним стилем" у python: def odd_words (слова): return [x for x in words if ned (len (x))]]
в полі

@boxed: Ваше odd_words(words)визначення чимось відрізняється від відповіді allOdd. Для фільтрації та відображення часто бажають розуміння списків, але тут функція allOddповинна зменшити список слів до одного булевого значення.
ShinNoNoir

@WReach: Я б написав ваш функціональний приклад так: функція allOdd (слова) {return і (непарні (довжина (перший (слова)))))), allOdd (решта (слова))); } Це не більш елегантно, ніж ваш приклад, але в рекурсивній мові він має такі ж характеристики, як і імперативний стиль.
mishoo

@mishoo Мова має бути як рекурсивним, так і суворим та коротким замиканням в операторі, і, на вашу думку, я вважаю.
kqr

46

Справжня різниця між функціональним та імперативним програмуванням полягає у мисленні - імперативні програмісти думають про змінні та блоки пам'яті, тоді як функціональні програмісти думають: "Як я можу перетворити вхідні дані у свої вихідні дані" - ваша "програма" - це конвеєр і набір перетворень на дані, щоб взяти їх з вводу на вихід. Це цікава частина IMO, а не біт "Ти не будеш використовувати змінні".

Як наслідок цього мислення, програми FP зазвичай описують те, що буде, а не конкретний механізм того, як це станеться - це потужно, тому що якщо ми можемо чітко заявити, що означає "Вибрати" та "Де" та "Сукупність", ми ми можемо поміняти свої реалізації, як і у нас з AsParallel (), і раптом наша однопоточна програма масштабується до n ядер.


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

1
@KerxPhilo: Ось дуже просте завдання (додайте числа від 1 до n). Імператив: Змініть поточне число, змініть наразі суму. Код: int i, сума; сума = 0; для (i = 1; i <= n; i ++) {sum + = i; }. Функціонал (Haskell): Візьміть лінивий список чисел, складіть їх разом, додаючи до нуля. Код: foldl (+) 0 [1..n]. На жаль, форматування в коментарях відсутнє
dirkt

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

12
     Isn't that the same exact case for procedural programming?

Ні, оскільки процесуальний кодекс може мати побічні ефекти. Наприклад, він може зберігати стан між дзвінками.

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


1
Чи можете ви показати приклад та порівняння? По-справжньому оцініть це, якщо зможете.
Філоксофер

8
Функція rand () в C забезпечує різний результат для кожного дзвінка. Він зберігає стан між дзвінками. Він не референтно прозорий. Для порівняння, std :: max (a, b) в C ++ завжди буде повертати той самий результат, що дається однаковими аргументами, і не має побічних ефектів (про які я знаю ...).
Енді Томас

11

Я не згоден з відповіддю WReach. Давайте трохи розберемо його відповідь, щоб побачити, звідки походить незгода.

По-перше, його код:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

і

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

Перше, що слід зазначити, це те, що він плутає:

  • Функціональний
  • Експресія орієнтована і
  • Ітератор зосереджений

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

Давайте швидко поговоримо про це.

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

lengths: map words length
each_odd: map lengths odd
all_odd: reduce each_odd and

Це майже те саме, що і раніше, за винятком того, що функції пов'язані виключно через ланцюжки висловлювань та прив’язок.

Стиль програмування, орієнтований на ітератор, може бути обраний Python. Давайте використаємо суто ітеративний, орієнтований на ітератор стиль:

def all_odd(words):
    lengths = (len(word) for word in words)
    each_odd = (odd(length) for length in lengths)
    return all(each_odd)

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

Звичайно, ви можете стиснути це:

def all_odd(words):
    return all(odd(len(word)) for word in words)

Зараз імператив виглядає не так погано, так? :)

Заключний пункт стосувався більш явного контролю потоку. Давайте перепишемо оригінальний код, щоб скористатися цим:

function allOdd(words) {
    for (var i = 0; i < length(words); ++i) {
        if (!odd(length(words[i]))) {
            return false;
        }
    }
    return true;
}

За допомогою ітераторів ви можете мати:

function allOdd(words) {
    for (word : words) { if (!odd(length(word))) { return false; } }
    return true;
}

Так що це точка функціональної мови , якщо різниця між:

return all(odd(len(word)) for word in words)
return apply(and, map(compose(odd, length), words))
for (word : words) { if (!odd(length(word))) { return false; } }
return true;


Основна остаточна особливість функціональної мови програмування полягає в тому, що вона видаляє мутацію як частину типової моделі програмування. Люди часто розуміють це тим, що функціональна мова програмування не має висловлювань або використовує вирази, але це спрощення. Функціональна мова замінює явні обчислення декларацією поведінки, на якій мова потім виконує скорочення.

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

Коли у вас є функціональна мова, створення нових функцій, як правило, так само просто, як і складання тісно пов’язаних функцій.

all = partial(apply, and)

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


Ви знаєте, я впевнений, що applyце не така сама операція, як foldабо reduce, хоча я згоден з приємною здатністю мати дуже загальні алгоритми.
Бенедикт Лі

Я ніколи не чув про те, applyщоб це означало foldабо reduce, але мені здається, що це повинно бути в цьому контексті, щоб він повернув булеву форму.
Ведрак

Ах, добре, я заплутався в називанні. Дякуємо, що очистили його.
Бенедикт Лі

6

У парадигмі процедур (чи варто сказати замість цього "структуроване програмування") ви поділилися змінною пам'яттю та інструкціями, які читають / записують її в певній послідовності (одна за одною).

У функціональній парадигмі у вас є змінні та функції (в математичному сенсі: змінні з часом не змінюються; функції можуть лише обчислити щось на основі їх вхідних даних).

(Це надто спрощено, наприклад, FPLs, як правило, мають засоби для роботи з змінною пам'яттю, тоді як процедурні мови часто можуть підтримувати процедури вищого порядку, щоб все не було настільки чітким, але це повинно дати вам уявлення)


2

Чарівний Python: Функціональне програмування на Python від IBM Developerworks дійсно допоміг мені зрозуміти різницю.

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


2

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

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


1

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


0

Однією чіткою різницею між двома парадигмами програмування є стан.

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

Приклад:

def double(x):
    return x * 2

def doubleLst(lst):
    return list(map(double, action))

Однак процедурне програмування використовує стан.

Приклад:

def doubleLst(lst):
    for i in range(len(lst)):
        lst[i] = lst[i] * 2  # assigning of value i.e. mutation of state
    return lst
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.