Làm thế nào một ngôn ngữ lập trình chức năng thuần túy quản lý mà không có câu lệnh gán?


26

Khi đọc SICP nổi tiếng, tôi thấy các tác giả có vẻ khá miễn cưỡng khi đưa ra tuyên bố chuyển nhượng cho Đề án trong Chương 3. Tôi đọc văn bản và loại hiểu lý do tại sao họ cảm thấy như vậy.

Vì Scheme là ngôn ngữ lập trình chức năng đầu tiên tôi từng biết, tôi rất ngạc nhiên khi có một số ngôn ngữ lập trình chức năng (không phải Scheme dĩ nhiên) có thể làm mà không cần gán.

Hãy sử dụng ví dụ cuốn sách cung cấp, bank accountví dụ. Nếu không có câu lệnh gán, làm thế nào điều này có thể được thực hiện? Làm thế nào để thay đổi balancebiến? Tôi hỏi như vậy bởi vì tôi biết có một số ngôn ngữ chức năng thuần túy ngoài kia và theo lý thuyết hoàn chỉnh Turing, điều này cũng có thể được thực hiện.

Tôi đã học C, Java, Python và sử dụng các bài tập rất nhiều trong mỗi chương trình tôi đã viết. Vì vậy, nó thực sự là một kinh nghiệm mở mắt. Tôi thực sự hy vọng ai đó có thể giải thích ngắn gọn về cách tránh các bài tập trong các ngôn ngữ lập trình chức năng đó và tác động sâu sắc (nếu có) của nó đối với các ngôn ngữ này.

Ví dụ được đề cập ở trên là đây:

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

Điều này đã thay đổi balancebởi set!. Đối với tôi nó trông giống như một phương thức lớp để thay đổi thành viên lớp balance.

Như tôi đã nói, tôi không quen thuộc với các ngôn ngữ lập trình chức năng, vì vậy nếu tôi nói điều gì đó sai về chúng, hãy thoải mái chỉ ra.


1
Về việc học một ngôn ngữ chức năng thuần túy: Tôi không nhất thiết khuyên bạn nên làm điều đó ngay lập tức. Nếu bạn học Haskell thì ngoài việc học cách viết chương trình mà không có biến đổi, bạn cũng sẽ phải học về sự lười biếng và cách của Haskell để thực hiện IO. Đó có thể là một chút tất cả cùng một lúc. Nếu bạn muốn học viết chương trình mà không có trạng thái có thể thay đổi, cách dễ nhất có lẽ là chỉ viết một loạt các chương trình chương trình mà không sử dụng set!hoặc các chức năng khác kết thúc bằng a !. Khi bạn cảm thấy thoải mái với điều đó, việc chuyển đổi sang FP thuần túy sẽ dễ dàng hơn.
sepp2k

Câu trả lời:


21

Nếu không có câu lệnh gán, làm thế nào điều này có thể được thực hiện? Làm thế nào để thay đổi biến số dư?

Bạn không thể thay đổi các biến mà không có một số toán tử gán.

Tôi hỏi như vậy bởi vì tôi biết có một số ngôn ngữ chức năng thuần túy ngoài kia và theo lý thuyết hoàn chỉnh Turing, điều này cũng có thể được thực hiện.

Không hẳn. Nếu một ngôn ngữ là Turing hoàn thành, điều đó có nghĩa là nó có thể tính toán bất cứ thứ gì mà bất kỳ ngôn ngữ hoàn chỉnh Turing nào khác có thể tính được. Điều đó không có nghĩa là nó phải có mọi tính năng mà các ngôn ngữ khác có.

Không có gì mâu thuẫn khi ngôn ngữ lập trình hoàn chỉnh Turing không có cách nào thay đổi giá trị của biến, miễn là với mọi chương trình có biến có thể thay đổi, bạn có thể viết chương trình tương đương không có biến có thể thay đổi (trong đó "tương đương" có nghĩa là rằng nó tính toán điều tương tự). Và trên thực tế mọi chương trình đều có thể được viết theo cách đó.

Về ví dụ của bạn: Trong một ngôn ngữ hoàn toàn có chức năng, bạn chỉ đơn giản là không thể viết một hàm trả về số dư tài khoản khác nhau mỗi lần nó được gọi. Nhưng bạn vẫn có thể viết lại mọi chương trình, sử dụng chức năng đó theo một cách khác.


Vì bạn đã hỏi một ví dụ, chúng ta hãy xem xét một chương trình bắt buộc sử dụng chức năng rút tiền của bạn (bằng mã giả). Chương trình này cho phép người dùng rút tiền từ tài khoản, gửi vào tài khoản hoặc truy vấn số tiền trong tài khoản:

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))

Đây là một cách để viết cùng một chương trình mà không sử dụng các biến có thể thay đổi (tôi sẽ không bận tâm với IO minh bạch tham chiếu vì câu hỏi không phải là về điều đó):

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)

Hàm tương tự cũng có thể được viết mà không sử dụng đệ quy bằng cách sử dụng một nếp gấp trên đầu vào của người dùng (sẽ có nhiều thành ngữ hơn đệ quy rõ ràng), nhưng tôi không biết liệu bạn có quen thuộc với các nếp gấp không, vì vậy tôi đã viết nó trong một cách mà không sử dụng bất cứ điều gì bạn chưa biết.


Tôi có thể thấy quan điểm của bạn nhưng hãy xem tôi muốn một chương trình mô phỏng tài khoản ngân hàng và cũng có thể thực hiện những việc này (rút tiền và gửi tiền), vậy có cách nào dễ dàng để làm điều này không?
Gnijuohz

@Gnijuohz Nó luôn phụ thuộc vào vấn đề chính xác mà bạn đang cố gắng giải quyết. Ví dụ: nếu bạn có số dư đầu kỳ và danh sách rút tiền và tiền gửi và bạn muốn biết số dư sau khi rút và gửi tiền, bạn chỉ cần tính tổng số tiền gửi trừ đi tổng số tiền rút và thêm vào số dư bắt đầu . Vì vậy, trong mã đó sẽ được newBalance = startingBalance + sum(deposits) - sum(withdrawals).
sepp2k

1
@Gnijuohz Tôi đã thêm một chương trình ví dụ vào câu trả lời của mình.
sepp2k

Cảm ơn thời gian và nỗ lực bạn viết và viết lại câu trả lời! :)
Gnijuohz

Tôi sẽ nói thêm rằng sử dụng tiếp tục cũng có thể là một phương tiện để đạt được điều đó trong sơ đồ (miễn là bạn có thể chuyển một đối số cho tiếp tục?)
dader51

11

Bạn đúng rằng nó trông rất giống một phương thức trên một đối tượng. Đó là bởi vì đó thực chất là những gì nó được. Các lambdachức năng là một đóng cửa mà kéo biến bên ngoài balancevào phạm vi của nó. Có nhiều lần đóng gần với (các) biến ngoài và có nhiều phương thức trên cùng một đối tượng là hai khái niệm trừu tượng khác nhau để thực hiện cùng một điều chính xác và một trong hai phương thức có thể được thực hiện theo cách khác nếu bạn hiểu cả hai mô hình.

Cách ngôn ngữ chức năng thuần túy xử lý nhà nước là bằng cách gian lận. Ví dụ, trong Haskell nếu bạn muốn đọc đầu vào từ nguồn bên ngoài, (tất nhiên là không đặc biệt, và sẽ không nhất thiết phải đưa ra kết quả tương tự hai lần nếu bạn lặp lại nó), nó sử dụng một mẹo đơn giản để nói "chúng tôi có biến giả vờ khác này đại diện cho trạng thái của toàn bộ phần còn lại của thế giới và chúng ta không thể kiểm tra nó trực tiếp, nhưng đọc đầu vào là một hàm thuần túy lấy trạng thái của thế giới bên ngoài và trả về đầu vào xác định đó là trạng thái chính xác sẽ luôn luôn kết xuất, cộng với trạng thái mới của thế giới bên ngoài. " (Đó là một lời giải thích đơn giản, tất nhiên. Đọc về cách nó thực sự hoạt động sẽ phá vỡ nghiêm trọng bộ não của bạn.)

Hoặc trong trường hợp xảy ra sự cố tài khoản ngân hàng của bạn, thay vì gán giá trị mới cho biến, nó có thể trả về giá trị mới dưới dạng kết quả của hàm, và sau đó người gọi phải xử lý theo kiểu chức năng, nói chung bằng cách tạo lại bất kỳ dữ liệu nào tham chiếu giá trị đó với phiên bản mới chứa giá trị được cập nhật. (Đây không phải là một hoạt động cồng kềnh như âm thanh nếu dữ liệu của bạn được thiết lập với cấu trúc cây phù hợp.)


Tôi thực sự quan tâm đến câu trả lời của chúng tôi và ví dụ về Haskell nhưng do thiếu kiến ​​thức về nó, tôi không thể hiểu hết phần cuối của câu trả lời của bạn (cũng là phần thứ hai :()
Gnijuohz

3
@Gnijuohz Đoạn cuối nói rằng thay vì b = makeWithdraw(42); b(1); b(2); b(3); print(b(4))bạn chỉ có thể làm b = 42; b1 = withdraw(b1, 1); b2 = withdraw(b1, 2); b3 = withdraw(b2, 3); print(withdraw(b3, 4));nơi withdrawđược định nghĩa đơn giản là withdraw(balance, amount) = balance - amount.
sepp2k

3

"Toán tử đa gán" là một ví dụ về tính năng ngôn ngữ, nói chung, có tác dụng phụ và không tương thích với một số thuộc tính hữu ích của ngôn ngữ chức năng (như đánh giá lười biếng).

Tuy nhiên, điều đó không có nghĩa là việc gán nói chung không tương thích với kiểu lập trình chức năng thuần túy ( ví dụ xem cuộc thảo luận này ), cũng không có nghĩa là bạn không thể xây dựng cú pháp cho phép các hành động trông giống như bài tập nói chung, nhưng được thực hiện mà không có tác dụng phụ. Tuy nhiên, việc tạo ra loại cú pháp đó và viết các chương trình hiệu quả trong đó rất tốn thời gian và khó khăn.

Trong ví dụ cụ thể của bạn, bạn đã đúng - bộ! toán tử một nhiệm vụ. Đây không phải là một toán tử miễn phí có tác dụng phụ và đó là nơi Scheme phá vỡ bằng cách tiếp cận hoàn toàn chức năng để lập trình.

Cuối cùng, bất kỳ ngôn ngữ hoàn toàn chức năng sẽ phải phá vỡ với cách tiếp cận đôi khi hoàn toàn chức năng - đại đa số các chương trình hữu ích làm có tác dụng phụ. Quyết định làm ở đâu thường là vấn đề thuận tiện và các nhà thiết kế ngôn ngữ sẽ cố gắng cung cấp cho người lập trình sự linh hoạt cao nhất trong việc quyết định nơi nào phá vỡ bằng cách tiếp cận chức năng thuần túy, phù hợp với chương trình và miền vấn đề của họ.


"Cuối cùng, đôi khi bất kỳ ngôn ngữ chức năng thuần túy nào cũng sẽ bị phá vỡ bằng cách tiếp cận chức năng thuần túy - phần lớn các chương trình hữu ích đều có tác dụng phụ" Đúng, nhưng sau đó bạn đang nói về việc thực hiện IO và như vậy. Rất nhiều chương trình hữu ích có thể được viết mà không có các biến có thể thay đổi.
sepp2k

1
... Và bởi "đại đa số" các chương trình hữu ích, ý bạn là "tất cả", phải không? Tôi đang gặp khó khăn khi tưởng tượng khả năng tồn tại của bất kỳ chương trình nào có thể được gọi là "hữu ích" một cách hợp lý mà không thực hiện I / O, một hành động đòi hỏi tác dụng phụ theo cả hai hướng.
Mason Wheeler

Các chương trình SQL @MasonWheeler không làm IO như vậy. Cũng không có gì lạ khi viết một loạt các hàm không thực hiện IO bằng ngôn ngữ có REPL và sau đó chỉ cần gọi chúng từ REPL. Điều này có thể hoàn toàn hữu ích nếu đối tượng mục tiêu của bạn có khả năng sử dụng REPL (đặc biệt nếu đối tượng mục tiêu của bạn là bạn).
sepp2k

1
@MasonWheeler: chỉ là một ví dụ đơn giản, đơn giản: tính toán khái niệm n chữ số của pi không yêu cầu bất kỳ I / O nào. Đó là "chỉ" toán học và các biến. Đầu vào yêu cầu duy nhất là n và giá trị trả về là Pi (đến n chữ số).
Joachim Sauer

1
@Joachim Sauer cuối cùng bạn sẽ muốn in kết quả ra màn hình hoặc báo cáo về nó cho người dùng. Và ban đầu bạn sẽ muốn tải một số hằng vào chương trình từ một nơi nào đó. Vì vậy, nếu bạn muốn trở thành người phạm tội, tất cả các chương trình hữu ích phải thực hiện IO tại một số điểm, ngay cả khi đó là những trường hợp tầm thường và luôn bị ẩn khỏi chương trình bởi môi trường
blueberryfields

3

Trong một ngôn ngữ chức năng thuần túy, người ta sẽ lập trình một đối tượng tài khoản ngân hàng như một chức năng biến đổi luồng. Đối tượng được coi là một chức năng từ một luồng yêu cầu vô hạn từ chủ sở hữu tài khoản (hoặc bất cứ ai) đến một luồng phản hồi có khả năng vô hạn. Hàm bắt đầu với số dư ban đầu và xử lý từng yêu cầu trong luồng đầu vào để tính toán số dư mới, sau đó được đưa trở lại cuộc gọi đệ quy để xử lý phần còn lại của luồng. (Tôi nhớ rằng SICP thảo luận về mô hình biến dòng trong một phần khác của cuốn sách.)

Một phiên bản phức tạp hơn của mô hình này được gọi là "lập trình phản ứng chức năng" được thảo luận ở đây trên StackOverflow .

Cách làm ngây thơ của máy biến dòng có một số vấn đề. Có thể (trên thực tế, khá dễ dàng) để viết các chương trình lỗi mà giữ tất cả các yêu cầu cũ xung quanh, lãng phí không gian. Nghiêm trọng hơn, có thể thực hiện phản hồi cho yêu cầu hiện tại phụ thuộc vào các yêu cầu trong tương lai. Giải pháp cho những vấn đề này hiện đang được thực hiện. Neel Krishnaswami là lực lượng đằng sau họ.

Tuyên bố miễn trừ trách nhiệm : Tôi không thuộc về nhà thờ lập trình chức năng thuần túy. Thực tế, tôi không thuộc về bất kỳ nhà thờ nào :-)


Tôi đoán bạn thuộc về một số ngôi đền? :-P
Gnijuohz

1
Ngôi đền của tư duy tự do. Không có người giảng đạo ở đó.
Uday Reddy

2

Không thể thực hiện một chương trình 100% chức năng nếu nó được cho là làm bất cứ điều gì hữu ích. . đầu ra cho bàn điều khiển). Điều đó nói rằng bạn có thể làm cho hầu hết các mã của bạn hoạt động và phần đó sẽ dễ dàng kiểm tra, thậm chí tự động. Sau đó, bạn tạo một số mã bắt buộc để thực hiện đầu vào / đầu ra / cơ sở dữ liệu / ... sẽ cần gỡ lỗi, nhưng giữ cho hầu hết các mã sạch sẽ, nó sẽ không quá nhiều công việc. Tôi sẽ sử dụng ví dụ rút tiền của bạn:

(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))

Có thể thực hiện tương tự trong hầu hết mọi ngôn ngữ và tạo ra kết quả giống nhau (ít lỗi hơn), mặc dù bạn có thể phải đặt các biến tạm thời trong một quy trình và thậm chí thay đổi công cụ, nhưng điều đó không quan trọng bằng chừng nào thủ tục thực sự hoạt động chức năng (chỉ các tham số xác định kết quả). Tôi tin rằng bạn trở thành một lập trình viên giỏi hơn ở bất kỳ ngôn ngữ nào sau khi bạn đã lập trình một chút LISP :)


+1 cho ví dụ mở rộng và giải thích thực tế về các phần chức năng và các phần chức năng không thuần túy của chương trình và đề cập đến lý do tại sao FP vẫn quan trọng.
Zelphir Kaltstahl

1

Chuyển nhượng là hoạt động xấu vì nó phân chia không gian trạng thái thành hai phần, trước khi gán và sau khi gán. Điều này gây ra khó khăn trong việc theo dõi các biến đang được thay đổi trong quá trình thực hiện chương trình. Những điều sau đây trong ngôn ngữ chức năng đang thay thế bài tập:

  1. Các tham số chức năng được liên kết trực tiếp với các giá trị trả về
  2. chọn các đối tượng khác nhau được trả về thay vì sửa đổi các đối tượng hiện có.
  3. tạo ra các giá trị đánh giá lười biếng mới
  4. liệt kê tất cả các đối tượng có thể , không chỉ những đối tượng cần có trong bộ nhớ
  5. không có tác dụng phụ

Điều này dường như không giải quyết câu hỏi đặt ra. Làm thế nào để bạn lập trình một đối tượng tài khoản ngân hàng bằng một ngôn ngữ chức năng thuần túy?
Uday Reddy

nó chỉ là các chức năng chuyển đổi từ hồ sơ tài khoản ngân hàng này sang hồ sơ tài khoản ngân hàng khác. Điều quan trọng là khi các phép biến đổi như vậy xảy ra, các đối tượng mới được chọn thay vì sửa đổi các đối tượng hiện có.
tp1

Khi bạn chuyển đổi một bản ghi tài khoản ngân hàng sang một bản ghi khác, bạn muốn khách hàng thực hiện giao dịch tiếp theo trên bản ghi mới, chứ không phải bản ghi cũ. "Điểm liên lạc" cho khách hàng phải liên tục được cập nhật để trỏ đến hồ sơ hiện tại. Đó là một ý tưởng cơ bản của "sửa đổi". Tài khoản ngân hàng "đối tượng" không phải là hồ sơ tài khoản ngân hàng.
Uday Reddy
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.