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


26

Читаючи знаменитий SICP, я виявив, що автори здаються досить неохоче ввести заяву про призначення в Схему в Розділі 3. Я читаю текст і розумію, чому вони так вважають.

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

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

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

Приклад, згаданий вище, тут:

(define (make-withdraw balance)
    (lambda (amount)
        (if (>= balance amount)
            (begin (set! balance (- balance amount))
                balance)
            "Insufficient funds")))

Це змінило balanceо set!. Для мене це дуже схоже на метод класу для зміни члена класу balance.

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


1
Щодо вивчення чисто функціональної мови: я б не обов'язково рекомендував робити це відразу. Якщо ви навчитеся Haskell, то крім того, як писати програми без змінних змінних, вам також доведеться дізнатися про лінь і спосіб Haskell виконувати IO. Це може бути трохи набагато все одночасно. Якщо ви хочете навчитися писати програми без змінного стану, мабуть, найпростішим способом було б просто написати купу програм програми без використання set!чи інших функцій, які закінчуються символом a !. Після того, як вам це буде комфортно, перехід на чистий FP повинен бути простішим.
sepp2k

Відповіді:


21

Якщо немає заяви про призначення, як це зробити? Як змінити змінну балансу?

Ви не можете змінити змінні без якогось оператора призначення.

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

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

Це не протиріччя, що повна мова програмування Тюрінга не може змінити значення змінної, якщо для кожної програми, що має змінні змінні, ви можете написати еквівалентну програму, яка не має змінних змінних (де "еквівалент" означає що він обчислює те саме). І насправді кожна програма може бути написана саме так.

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


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

account = make-withdraw(0)
ask for input until the user enters "quit"
    if the user entered "withdraw $x"
        account(x)
    if the user entered "deposit $x"
        account(-x)
    if the user entered "query"
        print("The balance of the account is " + account(0))

Ось спосіб написати ту саму програму без використання змінних змінних (я не буду морочитися з референтно прозорим IO, оскільки питання не було про це):

function IO_loop(balance):
    ask for input
    if the user entered "withdraw $x"
        IO_loop(balance - x)
    if the user entered "deposit $x"
        IO_loop(balance + x)
    if the user entered "query"
        print("The balance of the account is " + balance)
        IO_loop(balance)
    if the user entered "quit"
        do nothing

 IO_loop(0)

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


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

@Gnijuohz Завжди залежить від того, яку саме проблему ти намагаєшся вирішити. Наприклад, якщо у вас є стартовий баланс і перелік знятих та депозитів, і ви хочете дізнатися залишок після цих вилучень і депозитів, ви можете просто обчислити суму депозитів за мінусом суми зняття коштів і додати її до початкового балансу . Тож у коді це було б newBalance = startingBalance + sum(deposits) - sum(withdrawals).
sepp2k

1
@Gnijuohz До своєї відповіді я додав приклад програми.
sepp2k

Дякуємо за час та зусилля, які ви вклали у написання та переписування відповіді! :)
Gnijuohz

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

11

Ви маєте рацію, що це дуже схоже на метод на об'єкті. Це тому, що це по суті те, що воно є. lambdaФункція являє собою замикання , яка тягне зовнішню змінну balanceв її рамки. Маючи декілька закриттів, які закриваються над однією і тією ж зовнішньою змінною (ими) та мають кілька методів на одному об'єкті, це дві різні абстракції для виконання точно тієї ж речі, і будь-яка може бути реалізована з точки зору іншої, якщо ви розумієте обидві парадигми.

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

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


Мене дуже цікавить наша відповідь та приклад Хаскелла, але через відсутність знань про це я не можу повністю зрозуміти останню частину вашої відповіді (ну і другу частину :()
Gnijuohz

3
@Gnijuohz Останній абзац говорить про те, що замість b = makeWithdraw(42); b(1); b(2); b(3); print(b(4))вас можна просто робити b = 42; b1 = withdraw(b1, 1); b2 = withdraw(b1, 2); b3 = withdraw(b2, 3); print(withdraw(b3, 4));там, де withdrawпросто визначено як withdraw(balance, amount) = balance - amount.
sepp2k

3

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

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

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

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


"Зрештою, будь-яка суто функціональна мова коли-небудь повинна зірватися з чисто функціональним підходом - переважна більшість корисних програм має побічні ефекти" Правда, але тоді ви говорите про те, щоб робити IO і таке. Безліч корисних програм можна писати без змінних змінних.
sepp2k

1
... і "переважна більшість" корисних програм ви маєте на увазі "все", правда? Мені важко навіть уявити можливість існування будь-якої програми, яку можна було б розумно назвати "корисною", яка не виконує введення-виведення, дія, яке вимагає побічних ефектів в обох напрямках.
Мейсон Уілер

@MasonWheeler SQL-програми не роблять IO як такої. Також не рідкість писати купу функцій, які не роблять IO мовою, що має REPL, а потім просто викликають ті з REPL. Це може бути цілком корисно, якщо ваша цільова аудиторія здатна використовувати REPL (особливо якщо ваша цільова аудиторія - це ви).
sepp2k

1
@MasonWheeler: лише один простий зустрічний приклад: концептуально обчислення n цифр пі не вимагає жодного вводу / виводу. Це "лише" математика та змінні. Єдиним необхідним введенням є n, а повернене значення - Pi (до n цифр).
Йоахім Зауер

1
@Joachim Sauer зрештою ви захочете надрукувати результат на екрані або іншим чином повідомити про це користувачеві. І спочатку ви хочете звідкись завантажити деякі константи в програму. Отже, якщо ви хочете бути педантичними, всі корисні програми повинні робити IO в якийсь момент, навіть якщо це тривіальні випадки, які явні та завжди приховані від програміста оточенням
blueberryfields

3

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

Більш досконала версія цієї парадигми називається "функціональне реактивне програмування", про яку йдеться в StackOverflow .

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

Відмова : Я не належу до церкви чистого функціонального програмування. Насправді я не належу до жодної церкви :-)


Я думаю, ви належите до якогось храму? :-P
Gnijuohz

1
Храм вільного мислення. Ніяких проповідників там немає.
Удай Редді

2

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

(define +no-founds+ "Insufficient funds")

;; functional withdraw
(define (make-withdraw balance amount)
    (if (>= balance amount)
        (- balance amount)
        +no-founds+))

;; functional atm loop
(define (atm balance thunk)
  (let* ((amount (thunk balance)) 
         (new-balance (make-withdraw balance amount)))
    (if (eqv? new-balance +no-founds+)
        (cons +no-founds+ '())
        (cons (list 'withdraw amount 'balance new-balance) (atm new-balance thunk)))))

;; functional balance-line -> string 
(define (balance->string x)
  (if (eqv? x +no-founds+)
      (string-append +no-founds+ "\n")
      (if (null? x)
          "\n"
          (let ((first-token (car x)))
            (string-append
             (cond ((symbol? first-token) (symbol->string first-token))
                   (else (number->string first-token)))
             " "
             (balance->string (cdr x)))))))

;; functional thunk to test  
(define (input-10 x) 10) ;; define a purly functional input-method

;; since all procedures involved are functional 
;; we expect the same result every time.
;; we use this to test atm and make-withdraw
(apply string-append (map balance->string (atm 100 input-10)))

;; no program can be purly functional in any language.
;; From here on there are imperative dirty procedures!

;; A procedure to get input from user is needed. 
;; Side effects makes it imperative
(define (user-input balance)
  (display "You have $")
  (display balance)
  (display " founds. How much to withdraw? ")
  (read))

;; We need a procedure to print stuff to the console 
;; as well. Side effects makes it imperative
(define (pretty-print-result x)
  (for-each (lambda (x) (display (balance->string x))) x))

;; use imperative procedure with atm.
(pretty-print-result (atm 100 user-input))

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


+1 для широкого прикладу та реалістичних пояснень щодо функціональних частин та не чисто функціональних частин програми та зазначення того, чому FP все-таки має значення.
Зельфір Кальтшталь

1

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

  1. Параметри функції, пов'язані безпосередньо з поверненими значеннями
  2. вибір різних об’єктів, які потрібно повернути замість зміни існуючих об'єктів.
  3. створення нових цінностей, оцінених ледачими
  4. перелічуючи всі можливі об’єкти, а не лише ті, які потрібно мати в пам'яті
  5. відсутність побічних ефектів

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

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

Перетворюючи один запис банківського рахунку в інший, ви хочете, щоб клієнт здійснив наступну транзакцію на новій, а не на старій. "Точку контакту" для замовника необхідно постійно оновлювати, щоб вказати на поточний запис. Це фундаментальна ідея "модифікації". "Об'єкти" банківського рахунку - це не записи банківських рахунків.
Удай Редді
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.