Що робить функціональні мови програмування декларативними на відміну від імперативних?


17

У багатьох статтях, що описують переваги функціонального програмування, я бачив мови функціонального програмування, такі як Haskell, ML, Scala або Clojure, які називаються "декларативними мовами", відмінні від імперативних мов, таких як C / C ++ / C # / Java. Моє питання - що робить функціональні мови програмування декларативними, а не імперативними.

Пояснення, що описує відмінності між декларативним та імперативним програмуванням, часто зустрічається в тому, що в режимі імперативного програмування ви кажете комп'ютеру "Як щось робити" на відміну від "Що робити" в декларативних мовах. Проблема, яку я маю з цим поясненням, полягає в тому, що ви постійно робите обидва на всіх мовах програмування. Навіть якщо ви спускаєтесь до складання найнижчого рівня, ви все ще говорите комп’ютеру "Що робити", ви говорите процесору додати два числа, ви не вказуєте, як виконати додавання. Якщо ми підемо на інший кінець спектру, чистий функціональний мова високого рівня, як Haskell, ти насправді кажеш комп’ютеру, як досягти конкретного завдання, це те, що ваша програма - це послідовність інструкцій для досягнення конкретного завдання, якого комп'ютер не знає, як досягти самостійно. Я розумію, що такі мови, як Haskell, Clojure тощо, явно вищий рівень, ніж C / C ++ / C # / Java, і пропонують такі функції, як ледача оцінка, незмінні структури даних, анонімні функції, currying, стійкі структури даних тощо. функціональне програмування можливе та ефективне, але я б не класифікував їх як декларативні мови.

Чистою мовою декларацій для мене була б така, яка повністю складається лише з декларацій, прикладом таких мов є CSS (Так, я знаю, що CSS технічно не була б мовою програмування). CSS просто містить декларації стилів, які використовуються HTML та Javascript сторінки. CSS не може робити нічого іншого, крім робити декларації, він не може створювати функції класу, тобто функції, які визначають стиль відображення на основі якогось параметра, ви не можете виконувати сценарії CSS тощо. мова програмування ).

Оновлення:

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

Моя думка в тому, що в пролозі немає ніяких імперативних вказівок, ви в основному розповідаєте (декларуєте) комп'ютеру те, що він знає, а потім запитуєте (запитуєте) про знання. У функціональних мовах програмування ви все ще даєте вказівки, тобто приймаєте значення, викликаєте функцію X і додаєте до неї 1 тощо, навіть якщо ви безпосередньо не маніпулюєте місцями пам'яті або виписуєте обчислення покроково. Я б не сказав, що програмування в Haskell, ML, Scala або Clojure є декларативним у цьому сенсі, хоча я можу помилятися. Чи належне, правдиве, чисте функціональне програмування декларативне в тому сенсі, який я описав вище.


Де @JimmyHoffa, коли він вам потрібен?

2
1. C # і сучасний C ++ - це дуже функціональні мови. 2. Подумайте про різницю написання SQL та Java для досягнення подібної мети (можливо, якийсь складний запит із приєднанням). Ви також можете подумати, чим LINQ в C # відрізняється від написання простих циклів передбачення ...
AK_

також різниця - це більше питання стилю, ніж щось чітко визначене ... якщо ви не використовуєте prolog ...
AK_

1
Одним із ключів розпізнавання різниці є те, коли ви використовуєте оператора призначення , порівняно з оператором визначення / декларування - у Haskell, наприклад , немає оператора присвоєння . Поведінка цих двох операторів дуже відрізняється і змушує вас по-різному проектувати свій код.
Джиммі Хоффа

1
На жаль, навіть без оператора призначення я можу це зробити (let [x 1] (let [x (+ x 2)] (let [x (* x x)] x)))(сподіваюся, ви це зрозуміли, Clojure). Моє оригінальне запитання - чим це відрізняється від цього int x = 1; x += 2; x *= x; return x;На мою думку, це здебільшого те саме.
ALXGTV

Відповіді:


16

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

Питання полягає в тому, чи є така перспектива розумною, і, навпаки, наскільки послідовність інструкцій нагадує оголошення обчисленого результату. Для CSS декларативна перспектива явно корисніша. Для C явно переважає імперативна перспектива. Що стосується мов, як Haskell, ну ...

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

Однак те, як написані програми Haskell, їх часто можна з розумом читати як опис результату, який потрібно обчислити. Візьмемо, наприклад, програму для обчислення суми перших N фактичних даних:

sum_of_fac n = sum [product [1..i] | i <- ..n]

Ви можете desugar це і читати його як послідовність операцій STG, але набагато більш природно, щоб прочитати його як опис результату (який, я думаю, більш корисне визначення декларативного програмування , ніж «що для обчислення»): Результатом є сума добутків [1..i]для всіх i= 0,…, n. І це набагато декларативніше, ніж майже будь-яка програма чи функція C.


1
Причиною, що я задав це питання, є те, що багато людей намагаються просувати функціональне програмування як якусь нову виразну парадигму, повністю відокремлену від парадигм C / C ++ / C # / Java або навіть Python, коли насправді функціональне програмування - це просто ще одна форма вищого рівня імперативне програмування.
ALXGTV

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

6
@ALXGTV Функціональне програмування - це зовсім інша парадигма, навіть якщо ви налаштовані читати це як імператив (це дуже широка класифікація, є більше парадигм програмування, ніж цього). А інший код , який будує на цьому коді може бути прочитана декларативно теж: наприклад, map (sum_of_fac . read) (lines fileContent). Звичайно, в якийсь момент введення / виведення вступає в гру, але, як я вже сказав, це континуум. Частини програм Haskell є впевненішими, точно так само, як і C має синтаксис своєрідно-декларативного вираження ( x + y, ні load x; load y; add;)

1
@ALXGTV Ваша інтуїція правильна: декларативне програмування "просто" забезпечує більш високий рівень абстрагування певних імперативних деталей. Я б сказав, що це велика справа!
Андрес Ф.

1
@Brendan Я не думаю, що функціональне програмування - це підмножина імперативного програмування (а також незмінність не є обов'язковою ознакою FP). Можна стверджувати, що у випадку чистого, статично типового FP - це набір імперативних програмувань, де ефекти явні в типах. Ви знаєте твердження про що-небудь в щоку, що Хаскелл є "найкращою імперативною мовою у світі";)
Андрес Ф.

21

Основна одиниця імперативної програми - констатація . Заяви виконуються для їх побічних ефектів. Вони змінюють стан, який вони отримують. Послідовність висловлювань - це послідовність команд, позначаючи робити це потім робити це. Програміст задає точний порядок виконання обчислень. Це те, що люди мають на увазі, розповідаючи комп’ютеру, як це зробити.

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

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


1
Це була б ідеальна відповідь, якби вона містила приклад. Кращий приклад , який я знаю , щоб проілюструвати декларативний проти імперативу Linq проти foreach.
Роберт Харві

7

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

Ви припускаєте, що CSS - це "декларативна мова", я б взагалі не називав це мовою. Це формат структури даних, як-от JSON, XML, CSV або INI, просто формат для визначення даних, відомих перекладача певного характеру.

Деякі цікаві побічні ефекти трапляються, коли ви виймаєте оператора призначення з мови, і це справжня причина втрати всіх цих імперативних інструкцій step1-step2-step3 в декларативних мовах.

Що ви робите з оператором призначення у функції? Ви створюєте проміжні кроки. У цьому суть, оператор присвоєння використовується для покрокової зміни даних. Як тільки ви більше не можете змінювати дані, ви втрачаєте всі ці кроки і закінчуєте:

Кожна функція має лише одне твердження, це твердження є єдиною заявою

Зараз існує багато способів зробити одне твердження схожим на багато тверджень, але це просто хитрість, наприклад: 1 + 3 + (2*4) + 8 - (7 / (8*3))це очевидно одне твердження, але якщо ви пишете це як ...

1 +
3 +
(
  2*4
) +
8 - 
(
  7 / (8*3)
)

Це може спричинити появу послідовності операцій, які можуть бути простішими для мозку розпізнати потрібний розпад, який мав намір автор. Я часто роблю це в C # з таким кодом ..

people
  .Where(person => person.MiddleName != null)
  .Select(person => person.MiddleName)
  .Distinct();

Це єдине твердження, але показує появу розкладеного на багатьох - зауважте, присвоєнь немає.

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

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

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

sum xs = (head xs) + sum (tail xs)

Ви не можете створити resultSumзмінну і додати до неї під час проходження циклу xs, ви повинні просто взяти перше значення і додати його до будь-якої суми всього іншого - і отримати доступ до всього іншого, що вам потрібно перейти xsдо функції, tailоскільки ви не можете просто створити змінну для xs і відскочити голову, що було б імперативним підходом. (Так, я знаю, що ви можете використовувати руйнування, але цей приклад повинен бути наочним)


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

1 + 3 + (2 * 4) + 8 - (7 / (8 * 3)) це насправді кілька висловлювань / виразів, звичайно, це залежить від того, що ви розглядаєте як єдиний вираз. Підсумовуючи це, функціональне програмування - це в основному програмування без крапки з комою.
ALXGTV

1
@ALXGTV різниця між цим декларацією та імперативним стилем полягає в тому, що якби цей математичний вираз був висловлюванням, було б обов'язково, щоб вони були виконані як написані. Однак тому, що це не послідовність імперативних висловлювань, а скоріше вираз, що визначає те, що ви хочете, оскільки воно не говорить про те , як це не обов'язково, щоб воно було виконано так, як написано. Тому компіляція може зменшити вирази або навіть запам'ятати її як буквальне. Імперативні мови роблять це теж, але лише тоді, коли ви вкладаєте це в одне твердження; декларація.
Джиммі Хоффа

@ALXGTV Декларації визначають відносини, відносини є корисними; якщо x = 1 + 2тоді x = 3і x = 4 - 1. Послідовні висловлювання визначають інструкції, тому коли у x = (y = 1; z = 2 + y; return z;)вас більше немає чого-ковкого, те, що компілятор може робити декількома способами - швидше, обов'язково компілятор виконує те, що вам там доручили, тому що компілятор не може знати всіх побічних ефектів ваших інструкцій, щоб він міг " t змінити їх.
Джиммі Хоффа

Ви маєте справедливу точку.
ALXGTV

6

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

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

Візьмемо простий код a = b + cза основу і переглянемо вислів кількома різними мовами, щоб отримати ідею:

Коли ми пишемо a = b + cімперативною мовою, як-от C, ми присвоюємо поточне значення b + cзмінній aі нічого більше. Ми не робимо жодних принципових тверджень про те, що aє. Швидше, ми просто виконуємо крок у процесі.

Коли ми пишемо a = b + cв декларативну мову, як Microsoft Excel, (так, Excel є мовою програмування , і , ймовірно, самий декларативний з них усіх,) ми стверджувати , відносини між a, bі cтаким чином, що це завжди так , що aце сума інші два. Це не крок у процесі, це інваріант, гарантія, декларація істини.

Функціональні мови також є декларативними, але майже випадково. У Хаскела, наприклад , a = b + cтакож стверджує , інваріантні відносини, але тільки тому , що bі cнезмінні.

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


0

Ви праві, що немає чіткого розрізнення між тим, щоб сказати комп’ютеру, що робити, і як це зробити.

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

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

У мовах, таких як Haskell, OTOH, пам'ять повністю втрачається. Це просто не так, що в Росії

f a b = let y = sum [a..b] in y*y

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

Але справа в тому, що нам не потрібно інтерналізувати архітектуру проти Неймана, щоб зрозуміти вищевказану функцію. Також нам не потрібен сучасний комп’ютер, щоб запустити його. Легко було б перекласти програми чистою мовою FP на гіпотетичну машину, яка працює, наприклад, на основі обчислення SKI. Тепер спробуйте те ж саме з програмою C!

Чистою мовою декларацій для мене була б така, яка повністю складається лише з декларацій

Це недостатньо сильно, ІМХО. Навіть програма C - це лише послідовність декларацій. Я вважаю, що ми повинні кваліфікувати декларації далі. Наприклад, вони кажуть нам , що - то є (декларативний) або те , що він робить (імператив).


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