Як працюють мови функціонального програмування?


92

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

Наприклад: як би ця проста C річ переклалася на функціональну мову програмування, як Haskell?

#include<stdio.h>
int main() {
    int no;
    scanf("%d",&no);
    return 0;
}

(Моє запитання було натхнене цим чудовим дописом: "Виконання в Королівстві іменників" . Прочитавши його, я трохи глибше зрозумів, що саме таке об’єктно-орієнтоване програмування, як Java реалізує його в одній крайній манері та як функціональні мови програмування є контраст.)



4
Це гарне запитання, оскільки на системному рівні комп’ютеру потрібен стан, щоб бути корисним. Я дивився інтерв'ю з Саймоном Пейтон-Джонсом (одним із розробників Хаскелла), де він сказав, що комп'ютер, який коли-небудь працював із повністю програмним забезпеченням без громадянства, міг досягти лише одного: стати гарячим! Багато хороших відповідей нижче. Є дві основні стратегії: 1) Зробити нечисту мову. 2) Складіть хитрий план абстрактного стану, що і робить Хаскелл, по суті створюючи новий, трохи змінений Світ замість того, щоб модифікувати старий.
шкодить

14
Хіба SPJ не говорив про побічні ефекти там, а не про стан? Чисті обчислення мають безліч станів, прихованих у прив'язуванні аргументів та стеку викликів, але без побічних ефектів (наприклад, введення / виведення) не може зробити нічого корисного. Ці два моменти насправді досить різні - є безліч чистих, виразних кодів Хаскелла, і Stateмонада дуже елегантна; з іншого боку IO- потворний, брудний хак, який використовується лише з невдоволенням.
CA McCann,

4
camccann це правильно. У функціональних мовах багато стану. Це просто явно керується замість "моторошної дії на відстані", як у імперативних мовах.
ТІЛЬКИ МОЙ правильний ДУМКА

1
Тут може бути певна плутанина. Можливо, комп’ютерам потрібні ефекти, щоб бути корисними, але я думаю, що тут йдеться про мови програмування, а не про комп’ютери.
Conal

Відповіді:


80

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

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

let x = func value 3.14 20 "random"
in ...

Мені гарантовано, що значення xзавжди однаково ...: ніщо не може змінити його. Подібним чином, якщо у мене є функція f :: String -> Integer(функція, яка приймає рядок і повертає ціле число), я можу бути впевнений, що fвона не змінить свій аргумент, не змінить жодних глобальних змінних, не запише дані у файл тощо. Як сказав sepp2k у коментарі вище, ця незмінність дійсно корисна для міркувань щодо програм: ви пишете функції, які згортають, обертають та калічать ваші дані, повертаючи нові копії, щоб ви могли зв'язати їх ланцюгом, і можете бути впевнені, що жодна з цих викликів функцій може зробити щось "шкідливе". Ви знаєте, що xце завжди x, і вам не доведеться хвилюватися, що хтось писав x := foo barдесь між декларацієюx та його використання, бо це неможливо.

Що робити, якщо я хочу прочитати введення від користувача? Як сказав KennyTM, ідея полягає в тому, що нечиста функція - це чиста функція, яка передає весь світ як аргумент і повертає як її результат, так і світ. Звичайно, ви не хочете насправді робити це: з одного боку, це жахливо незграбно, а з іншого, що станеться, якщо я повторно використаю той самий світовий об'єкт? Тож це якось абстрагується. Haskell обробляє це з типом IO:

main :: IO ()
main = do str <- getLine
          let no = fst . head $ reads str :: Integer
          ...

Це говорить нам, що mainце дія введення-виведення, яка нічого не повертає; виконання цієї дії означає, що означає запуск програми Haskell. Правило полягає в тому, що типи IO ніколи не можуть уникнути дії IO; у цьому контексті ми вводимо цю дію за допомогою do. Таким чином, getLineповертає an IO String, про що можна думати двома способами: по-перше, як дію, яка під час запуску створює рядок; по-друге, як рядок, який "заплямований" IO, оскільки отриманий нечисто. Перше правильніше, але друге може бути більш корисним. Копія <-виймає Stringз IO Stringі зберігає в ньому, str- але оскільки ми вже виконуємо операцію введення-виведення, нам доведеться обернути її назад, щоб вона не могла "втекти". Наступний рядок намагається прочитати ціле число ( reads) і захоплює перший успішний збіг (fst . head); це все чисто (без введення-виведення), тому ми даємо йому назву з let no = .... Потім ми можемо використовувати як noі strв .... Таким чином, ми зберігаємо нечисті дані (від getLineдо str) та чисті дані ( let no = ...).

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

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

Тож це, мабуть, здавалося трохи обманом, якимось чином додаючи домішок до чистого Хаскелла. Але це не так - виявляється, ми можемо реалізувати тип вводу-виводу цілком у чистому Haskell (якщо нам це дано RealWorld). Ідея така: дія вводу-виводу IO type- це те саме, що функція RealWorld -> (type, RealWorld), яка приймає реальний світ і повертає як об’єкт типу, так typeі модифікований RealWorld. Потім ми визначаємо пару функцій, щоб ми могли використовувати цей тип, не сходячи з розуму:

return :: a -> IO a
return a = \rw -> (a,rw)

(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'

Перший дозволяє нам говорити про дії вводу-виводу, які нічого не роблять: return 3це дія введення-виведення, яка не запитує реальний світ, а просто повертається 3. >>=Оператор, оголошений «прив'язувати», дозволяють запускати дії введення - виведення. Він витягує значення з дії вводу-виводу, передає його та реальний світ через функцію і повертає результуючу дію вводу-виводу. Зверніть увагу, що >>=виконується наше правило, згідно з яким ніколи не дозволяється отримувати результати результатів введення-виведення.

Потім ми можемо перетворити вищезазначене mainв наступний звичайний набір функціональних додатків:

main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...

Перехід до виконання Haskell починається mainз початкового RealWorld, і ми готові! Все чисто, воно просто має вигадливий синтаксис.

[ Редагувати: Як зазначає @Conal , насправді це не те, що використовує Haskell для здійснення вводу- виводу. Ця модель руйнується, якщо ви додаєте паралельність або взагалі будь-який спосіб, щоб світ змінився в середині дії вводу-виводу, тому для Хаскелла було б неможливо використовувати цю модель. Він точний лише для послідовних обчислень. Таким чином, може бути, що IO Хаскелла - трохи ухилення; навіть якщо це не так, це, звичайно, не зовсім це елегантно. За спостереженнями @ Conal, подивіться, що говорить Саймон Пейтон-Джонс у " Розв'язанні незручного загону" [pdf] , розділ 3.1; він представляє те, що могло б означати альтернативну модель за цими напрямками, але потім відмовляється від неї через її складність і бере інший варіант.]

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

Отож останнє: виявляється, ця структура - параметричний тип з returnі >>=- дуже загальна; це називається монада, і doпозначення return, і >>=робота з будь-якою з них. Як ви побачили тут, монади не є чарівними; все, що є магічним, це те, що doблоки перетворюються на виклики функцій. RealWorldТипом є єдиним місцем , ми бачимо диво. Такі типи, як []конструктор списку, також є монадами, і вони не мають нічого спільного з нечистим кодом.

Тепер ви знаєте (майже) все про поняття монади (крім кількох законів, які повинні бути виконані, та формального математичного визначення), але вам бракує інтуїції. В Інтернеті існує безглузда кількість підручників з монади; Мені подобається цей , але у вас є варіанти. Однак це, мабуть, вам не допоможе ; єдиний реальний спосіб отримати інтуїцію - це поєднання їх використання та читання декількох підручників у потрібний час.

Однак вам не потрібна ця інтуїція, щоб зрозуміти IO . Розуміння монад у цілому загалом - це вишенька, але ви можете використовувати IO прямо зараз. Ви можете використовувати його після того, як я показав вам першу mainфункцію. Ви навіть можете поводитися з кодом введення-виведення так, ніби він написаний нечистою мовою! Але пам’ятайте, що існує основне функціональне уявлення: ніхто не обманює.

(PS: Вибачте за довжину. Я пішов трохи далеко).


6
Мене завжди здивує Хаскелл (який я докладав і докладаю зусиль для вивчення) - це потворність синтаксису. Це так, ніби вони взяли найгірші шматочки будь-якої іншої мови, скинули їх у відро і люто перемішали. І ці люди потім будуть скаржитися на дивовижний синтаксис С ++ (місцями)!

19
Ніл: Справді? Я насправді вважаю синтаксис Хаскелла дуже чистим. Мені цікаво; про що конкретно ви маєте на увазі? (Для чого це варте, C ++ мене теж насправді не турбує, за винятком необхідності робити це > >в шаблонах.)
Antal Spector-Zabusky

6
На мій погляд, хоча синтаксис Хаскелла не такий чистий, як, скажімо, схема, він не починає порівнюватися з огидним синтаксисом, ну, навіть найприємнішої з фігурних дужок, серед яких C ++ є однією з найгірших . Ніякого обліку смаку, я припускаю. Я не думаю, що існує мова, котра всім здається синтаксично приємною.
CA McCann

8
@NeilButterworth: Я підозрюю, що ваша проблема полягає не стільки в синтаксисі, скільки в назвах функцій. Якби функції на зразок >>=або $мали більше, де замість цього викликали bindі apply, код haskell виглядав би набагато менше як perl. Я маю на увазі головну відмінність між синтаксисом haskell та схеми полягає в тому, що haskell має інфікс-оператори та необов’язкові парени. Якщо люди утримаються від надмірного використання операторів інфіксів, haskell буде схожий на схему з меншою кількістю парен.
sepp2k

5
@camcann: Ну, суть, але я мав на увазі: основний синтаксис схеми - (functionName arg1 arg2). Якщо видалити паренси, це functionName arg1 arg2синтаксис haskell. Якщо ви дозволите оператори інфіксів із довільно жахливими іменами, ви отримаєте, arg1 §$%&/*°^? arg2що навіть більше нагадує haskell. (Я просто дражуюсь до речі, мені насправді подобається haskell).
sepp2k

23

Тут багато хороших відповідей, але вони довгі. Я спробую дати корисну коротку відповідь:

  • Функціональні мови ставлять стан в ті самі місця, що і C: в іменованих змінних та в об'єктах, виділених у купі. Відмінності полягають у тому, що:

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

    • "Зміни стану" обробляються не шляхом мутації існуючих змінних або об'єктів, а шляхом прив'язки нових змінних або виділення нових об'єктів.

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

    • Жодна програма не може зробити копію Світу (куди б ви її поставили?)

    • Жодна програма не може викинути Світ

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

    Цей трюк прекрасно пояснюють Саймон Пейтон Джонс та Філ Уодлер у своєму знаковому документі "Імперативне функціональне програмування" .


4
Наскільки я можу зрозуміти, ця IOісторія ( World -> (a,World)) є міфом, коли застосовується до Haskell, оскільки ця модель пояснює лише суто послідовні обчислення, тоді як IOтип Haskell включає паралельність. Під "чисто послідовним" я маю на увазі, що навіть світу (Всесвіту) не дозволяється змінюватись між початком і кінцем імперативного обчислення, крім завдяки цьому обчисленню. Наприклад, поки ваш комп’ютер відхиляється, ваш мозок тощо не може. З паралельністю можна впоратись чимось більш подібним World -> PowerSet [(a,World)], що дозволяє не визначити детермінантність та чергування.
Конал

1
@Conal: Я думаю, що історія вводу-виводу досить добре узагальнює до недетермінізму та чергування; якщо я добре пам’ятаю, у статті «Незграбний загін» є досить гарне пояснення. Але я не знаю хорошої статті, яка чітко пояснює справжній паралелізм.
Норман Ремзі,

3
Наскільки я розумію, стаття "Незграбний загін" відмовляється від спроби узагальнити просту денотаційну модель IO, тобто World -> (a,World)(популярний і стійкий "міф", про який я згадував), і натомість дає оперативне пояснення. Деякі люди люблять оперативну семантику, але вони залишають мене абсолютно незадоволеним. Будь ласка, дивіться мою довшу відповідь в іншій відповіді.
Конал

+1 Це допомогло мені набагато більше зрозуміти IO Monads, а також відповісти на запитання.
CaptainCasey

Більшість компіляторів Haskell насправді визначають IOяк RealWorld -> (a,RealWorld), але замість того, щоб насправді представляти реальний світ, це просто абстрактне значення, яке має передаватися, і в кінцевому підсумку отримує оптимізація компілятором.
Джеремі Ліст

19

Я обриваю відповідь на нову відповідь, щоб дати більше місця:

Я написав:

Наскільки я можу зрозуміти, ця IOісторія ( World -> (a,World)) є міфом, коли застосовується до Haskell, оскільки ця модель пояснює лише суто послідовні обчислення, тоді як IOтип Haskell включає паралельність. Під "чисто послідовним" я маю на увазі, що навіть світу (Всесвіту) не дозволяється змінюватись між початком і кінцем імперативного обчислення, крім завдяки цьому обчисленню. Наприклад, поки ваш комп’ютер відхиляється, ваш мозок тощо не може. З паралельністю можна впоратись чимось більш подібним World -> PowerSet [(a,World)], що дозволяє не визначити детермінантність та чергування.

Норман писав:

@Conal: Я думаю, історія вводу-виводу досить добре узагальнює до недетермінізму та чергування; якщо я добре пам’ятаю, у статті «Незграбний загін» є досить гарне пояснення. Але я не знаю хорошої статті, яка чітко пояснює справжній паралелізм.

@ Норман: Узагальнює в якому сенсі? Я припускаю, що денотаційна модель / пояснення, що зазвичай дається World -> (a,World),, не відповідає Haskell, IOоскільки вона не враховує недетермінованість та паралельність. Може існувати більш складна модель, яка підходить, наприклад World -> PowerSet [(a,World)], але я не знаю, чи така модель була розроблена та показана адекватною та послідовною. Я особисто сумніваюся, що такого звіра можна знайти, враховуючи, що IOвін заселений тисячами імпортованих FFI викликів API. І як такий, IOвиконує своє призначення:

Відкрита проблема: IOмонада перетворилася на синагому Хаскелла. (Щоразу, коли ми чогось не розуміємо, ми кидаємо це в монаду IO.)

(З виступу POPL Саймона Піджея Носіння зачіски Носіння зачіски: ретроспектива на Haskell .)

У Розділі 3.1 " Розв'язання незручного загону" Саймон вказує на те, що не працює type IO a = World -> (a, World), зокрема, "Підхід погано масштабується, коли ми додаємо паралельність". Потім він пропонує можливу альтернативну модель, а потім відмовляється від спроби денотаційних пояснень, кажучи

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

Ця невдача знайти точну та корисну денотаційну модель лежить в основі того, чому я бачу Haskell IO як відхід від духу та глибоких переваг того, що ми називаємо "функціональним програмуванням", або тим, що Пітер Ландін більш конкретно назвав "денотативним програмуванням" . Дивіться коментарі тут.


Дякую за довшу відповідь. Думаю, можливо, мені промили мозок наші нові оперативні керівники. Лівий та правий рушії тощо дозволяють довести деякі корисні теореми. Ви бачили будь-яку денотаційну модель, яка вам подобається, яка б пояснювала недетермінованість та одночасність? Я не маю.
Норман Ремзі

1
Мені подобається, як World -> PowerSet [World]чітко фіксується недетермінізм та паралельність у стилі чергування. Це визначення домену говорить мені, що загальнообов’язкове імперативне програмування (включаючи програму Хаскелла) є нерозв’язним - буквально експоненціально складнішим, ніж послідовне. Велика шкода, яку я бачу в IOміфі про Хаскелла , приховує цю невід’ємну складність, демотивуючи її повалення.
Конал

Поки я бачу, чому World -> (a, World)порушено, мені незрозуміло, чому заміна World -> PowerSet [(a,World)]належним чином моделює паралельність тощо. Для мене це, мабуть, означає, що програми в IOповинні працювати в чомусь на кшталт монади списку, застосовуючи себе до кожного елемента, що повертається за IOдією. Чого мені не вистачає?
Antal Spector-Zabusky

3
@Absz: По-перше, запропонована мною модель World -> PowerSet [(a,World)]не є правильною. Давайте спробуємо World -> PowerSet ([World],a)замість цього. PowerSetдає набір можливих результатів (недетермінованість). [World]це послідовності проміжних станів (не монада списку / недетермінізму), що дозволяють чергувати (планування потоків). І ([World],a)це також не зовсім правильно, оскільки він дозволяє отримати доступ до того, aяк пройти всі проміжні стани. Натомість визначте використання World -> PowerSet (Computation a)деdata Computation a = Result a | Step World (Computation a)
Conal

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

17

Функціональне програмування походить від лямбда-числення. Якщо ви справді хочете зрозуміти функціональне програмування, відвідайте http://worrydream.com/AlligatorEggs/

Це «веселий» спосіб вивчити лямбда-числення і привести вас у захоплюючий світ функціонального програмування!

Наскільки знання Лямбда-числення корисне у функціональному програмуванні.

Отже, Лямбда-числення є основою для багатьох реальних мов програмування, таких як Lisp, Scheme, ML, Haskell, ....

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

plus3 x = succ(succ(succ x)) 

Прочитайте "plus3 - це функція, яка при застосуванні до будь-якого числа x дає наступника наступника наступника x"

Зверніть увагу, що функцію, яка додає 3 до будь-якого числа, не потрібно називати плюс3; назва «плюс3» - це просто зручний скорочений запис для іменування цієї функції

(plus3 x) (succ 0) ≡ ((λ x. (succ (succ (succ x)))) (succ 0))

Зверніть увагу, що ми використовуємо лямбда-символ для функції (я думаю, це виглядає як Алігатор, я здогадуюсь, саме звідси виникла ідея для яєць Алігатора)

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

Тепер розглянемо абстракцію:

g  λ f. (f (f (succ 0)))

Аргумент f використовується у позиції функції (у виклику). Ми називаємо функцію вищого порядку ga, оскільки вона приймає іншу функцію як вхід. Ви можете думати про інші виклики функції f як " яйця ". Тепер беремо дві функції або " Алігатори створені нами ", ми можемо зробити щось подібне:

(g plus3) =  f. (f (f (succ 0)))(λ x . (succ (succ (succ x)))) 
= ((λ x. (succ (succ (succ x)))((λ x. (succ (succ (succ x)))) (succ 0)))
 = ((λ x. (succ (succ (succ x)))) (succ (succ (succ (succ 0)))))
 = (succ (succ (succ (succ (succ (succ (succ 0)))))))

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

Тоді ви можете скористатися цим простим набором правил "їжі" Алігаторів " Алігаторів ", щоб розробити граматику, і таким чином народилися Функціональні мови програмування!

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


@tuckster: Я вже багато разів вивчав лямбда-числення ... і так, стаття AlligatorEggs для мене має сенс. Але я не можу пов'язати це з програмуванням. Для мене зараз лабораторне обчислення - це як окрема теорія, яка просто існує. Як поняття лямбда-числення використовуються в мовах програмування?
Лазер

3
@eSKay: Haskell - це лямбда-числення з тонким шаром синтаксичного цукру, щоб зробити його більш схожим на звичайну мову програмування. Мови сімейства Лісп також дуже схожі на нетипізований лямбда-числення, що і представляє Яйця Алігатора. Лямбда-числення саме по суті є мінімалістичною мовою програмування, якось схожою на "мову збірки функціонального програмування".
CA McCann

@eSKay: Я додав трохи про те, як це стосується, на деяких прикладах. Сподіваюся, це допоможе!
PJT

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

14

Техніка обробки станів у Хаскелі дуже проста. І вам не потрібно розуміти монади, щоб зрозуміти це.

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

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

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

Це також працює з введенням / виведенням. По суті, трапляється так: замість того, щоб отримувати введення від користувача деяким прямим еквівалентом scanfі зберігати його десь, ви замість цього пишете функцію, щоб сказати, що ви зробите з результатом, scanfякщо б у вас був, а потім передайте це функція до API вводу-виводу. Це саме те, що >>=відбувається, коли ви використовуєте IOмонаду в Haskell. Тому вам ніколи не потрібно зберігати результат будь-якого вводу-виводу в будь-якому місці - вам просто потрібно написати код, який говорить про те, як ви хочете його перетворити.


8

(Деякі функціональні мови дозволяють нечисті функції.)

Для чисто функціональних мов взаємодія в реальному світі зазвичай включається як один із аргументів функції, наприклад:

RealWorld pureScanf(RealWorld world, const char* format, ...);

Різні мови мають різні стратегії абстрагування світу від програміста. Наприклад, Хаскелл використовує монади, щоб приховати worldаргумент.


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

int compute_sum_of_squares (int min, int max) {
  int result = 0;
  for (int i = min; i < max; ++ i)
     result += i * i;  // modify "result" in place
  return result;
}

Ви включаєте частину модифікації у виклик функції, зазвичай перетворюючи цикли в рекурсії:

int compute_sum_of_squares (int min, int max) {
  if (min >= max)
    return 0;
  else
    return min * min + compute_sum_of_squares(min + 1, max);
}

Або просто computeSumOfSquares min max = sum [x*x | x <- [min..max]];-)
fredoverflow

@Fred: Розуміння списку - це просто синтаксичний цукор (і тоді вам потрібно детально пояснити монаду списку). А як ти реалізуєш sum? Рекурсія все ще потрібна.
kennytm

3

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

Наприклад, перевірити Хаскель державних Монад .


9
І майте на увазі, що ні про що StateніMonad що дає стан, так як вони обидва визначені в термінах простих, загальних, функціональних інструментів. Вони просто фіксують відповідні візерунки, тому вам не доведеться так багато винаходити колесо.
Конал


1

haskell:

main = do no <- readLn
          print (no + 1)

Звичайно, ви можете призначати речі змінним у функціональних мовах. Ви просто не можете їх змінити (тому в основному всі змінні є константами у функціональних мовах).


@ sepp2k: чому, яка шкода від їх зміни?
Лазер

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

9
@eSKay: Функціональні програмісти вважають, що мінливий стан створює багато можливостей для помилок і ускладнює міркування про поведінку програм. Наприклад, якщо у вас є виклик функції, f(x)і ви хочете побачити значення x, вам просто потрібно перейти до місця, де визначено x. Якби х було змінним, вам також слід було б подумати, чи є якийсь момент, коли х можна змінити між його визначенням та його використанням (що є нетривіальним, якщо х не є локальною змінною).
sepp2k

6
Не лише функціональні програмісти не довіряють змінному стану та побічним ефектам. Незмінні об’єкти та розділення команд / запитів добре розглядаються багатьма програмістами ОО, і майже всі вважають, що змінні глобальні змінні - погана ідея. Такі мови, як Хаскелл, просто переносять ідею далі, що більшість ...
CA McCann

5
@eSKay: Не стільки мутація шкідлива, скільки виявляється, якщо ти погодишся уникнути мутації, стає набагато простіше писати модульний код, який можна багаторазово використовувати. Без спільного змінного стану зв'язок між різними частинами коду стає явним, і набагато легше зрозуміти та підтримувати свій дизайн. Джон Хьюз пояснює це краще, ніж я можу; Візьміть його статтю Чому функціональне програмування важливо .
Норман Ремзі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.