Là lợi ích của mô hình đơn nguyên IO để xử lý các tác dụng phụ hoàn toàn là học thuật?


17

Xin lỗi vì đã có một câu hỏi về tác dụng phụ khác của FP +, nhưng tôi không thể tìm thấy câu hỏi nào đã trả lời cho tôi.

Sự hiểu biết (hạn chế) của tôi về lập trình chức năng là các tác dụng phụ / trạng thái nên được giảm thiểu và tách biệt khỏi logic phi trạng thái.

Tôi cũng thu thập cách tiếp cận của Haskell về vấn đề này, đơn vị IO, đạt được điều này bằng cách gói các hành động có trạng thái trong một thùng chứa, để thực hiện sau này, được xem xét bên ngoài phạm vi của chính chương trình.

Tôi đang cố gắng để hiểu mẫu này, nhưng thực sự để xác định có nên sử dụng nó trong dự án Python hay không, vì vậy muốn tránh các chi tiết cụ thể của Haskell nếu có.

Ví dụ thô đến.

Nếu chương trình của tôi chuyển đổi một tệp XML thành một tệp JSON:

def main():
    xml_data = read_file('input.xml')  # impure
    json_data = convert(xml_data)  # pure
    write_file('output.json', json_data) # impure

Không phải cách tiếp cận của đơn vị IO có hiệu quả để làm điều này:

steps = list(
    read_file,
    convert,
    write_file,
)

sau đó tự giải phóng trách nhiệm bằng cách không thực sự gọi các bước đó, nhưng để người phiên dịch làm việc đó?

Hoặc nói cách khác, nó giống như viết:

def main():  # pure
    def inner():  # impure
        xml_data = read_file('input.xml')
        json_data = convert(xml_data)
        write_file('output.json', json_data)
    return inner

sau đó mong đợi người khác gọi inner()và nói rằng công việc của bạn đã hoàn thành bởi vì đó main()là thuần túy.

Về cơ bản, toàn bộ chương trình sẽ được chứa trong đơn vị IO.

Khi mã được thực thi , mọi thứ sau khi đọc tệp phụ thuộc vào trạng thái của tệp đó, do đó vẫn sẽ gặp phải các lỗi liên quan đến trạng thái tương tự như việc triển khai bắt buộc, vậy bạn đã thực sự đạt được điều gì, với tư cách là một lập trình viên sẽ duy trì điều này?

Tôi hoàn toàn đánh giá cao lợi ích của việc giảm thiểucô lập hành vi trạng thái, đó là lý do tại sao tôi cấu trúc phiên bản bắt buộc như thế: thu thập dữ liệu đầu vào, làm công cụ thuần túy, nhổ đầu ra. Hy vọng convert()có thể hoàn toàn tinh khiết và gặt hái những lợi ích của bộ nhớ cache, an toàn luồng, v.v.

Tôi cũng đánh giá cao rằng các loại đơn nguyên có thể hữu ích, đặc biệt là trong các đường ống hoạt động trên các loại tương đương, nhưng không hiểu tại sao IO nên sử dụng các đơn vị trừ khi đã có trong một đường ống như vậy.

Có một số lợi ích bổ sung để xử lý các tác dụng phụ mà mẫu đơn nguyên IO mang lại, mà tôi đang thiếu?


1
Bạn nên xem video này . Kỳ quan của các đơn vị cuối cùng đã được tiết lộ mà không cần dùng đến Thể loại Lý thuyết hay Haskell. Nó chỉ ra rằng các đơn nguyên được thể hiện một cách tầm thường trong JavaScript và là một trong những yếu tố chính của Ajax. Monads là tuyệt vời. Chúng là những điều đơn giản, gần như được thực hiện một cách tầm thường, với sức mạnh to lớn để quản lý sự phức tạp. Nhưng hiểu chúng là khó khăn một cách đáng ngạc nhiên, và hầu hết mọi người, một khi họ có khoảnh khắc ah-ha đó, dường như mất khả năng giải thích chúng cho người khác.
Robert Harvey

Video tốt, cảm ơn. Tôi thực sự đã học được về những thứ này từ phần giới thiệu của JS đến lập trình chức năng (sau đó đọc thêm một triệu bài nữa). Mặc dù đã xem nó, tôi khá chắc chắn rằng câu hỏi của tôi là dành riêng cho đơn vị IO, mà Crock không bao gồm trong video đó.
Stu Cox

Hmm ... Không phải AJAX được coi là một dạng I / O sao?
Robert Harvey

1
Lưu ý rằng loại maintrong chương trình Haskell là IO ()- một hành động IO. Đây thực sự không phải là một chức năng; đó là một giá trị . Toàn bộ chương trình của bạn là một giá trị thuần túy chứa các hướng dẫn cho biết thời gian chạy ngôn ngữ nên làm gì. Tất cả những thứ không trong sạch (thực sự thực hiện các hành động IO) nằm ngoài phạm vi chương trình của bạn.
Wyzard

Trong ví dụ của bạn, phần đơn âm là khi bạn lấy kết quả của một phép tính ( read_file) và sử dụng nó làm đối số cho phần tiếp theo ( write_file). Nếu bạn chỉ có một chuỗi các hành động độc lập, bạn sẽ không cần Monad.
lortabac

Câu trả lời:


14

Về cơ bản, toàn bộ chương trình sẽ được chứa trong đơn vị IO.

Đó là nơi mà tôi nghĩ rằng bạn không nhìn thấy nó từ quan điểm của Haskeller. Vì vậy, chúng tôi có một chương trình như thế này:

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

Tôi nghĩ rằng một Haskeller điển hình đảm nhận điều này sẽ là convertphần thuần túy:

  1. Có lẽ là phần lớn của chương trình này, và phức tạp hơn nhiều so với các IOphần;
  2. Có thể được lý luận về và kiểm tra mà không phải đối phó với IOtất cả.

Vì vậy, họ không coi đây là convertviệc "chứa" trong IO, nhưng đúng hơn, vì nó được tách ra từ IO. Từ loại của nó, bất cứ điều gì convertkhông bao giờ có thể phụ thuộc vào bất cứ điều gì xảy ra trong một IOhành động.

Khi mã được thực thi, mọi thứ sau khi đọc tệp phụ thuộc vào trạng thái của tệp đó, do đó vẫn sẽ gặp phải các lỗi liên quan đến trạng thái tương tự như việc triển khai bắt buộc, vậy bạn đã thực sự đạt được điều gì, với tư cách là một lập trình viên sẽ duy trì điều này?

Tôi muốn nói rằng điều này chia thành hai điều:

  1. Khi chương trình chạy, giá trị của tham số để convertphụ thuộc vào tình trạng của tập tin.
  2. Nhưng convertchức năng làm gì , điều đó không phụ thuộc vào trạng thái của tệp. convertluôn luôn là cùng một hàm , ngay cả khi nó được gọi với các đối số khác nhau tại các điểm khác nhau.

Đây là một điểm hơi trừu tượng, nhưng nó thực sự quan trọng đối với ý nghĩa của Haskeller khi họ nói về điều này. Bạn muốn viết converttheo cách đưa ra bất kỳ đối số hợp lệ nào , nó sẽ tạo ra một kết quả chính xác cho đối số đó. Khi bạn nhìn vào nó như thế, thực tế là đọc một tệp là một hoạt động trạng thái không đi vào phương trình; tất cả vấn đề là bất cứ lý lẽ nào được đưa ra cho nó và bất cứ nơi nào có thể đến từ đó, convertphải xử lý nó một cách chính xác. Và thực tế là độ tinh khiết hạn chế những gì convertcó thể làm với đầu vào của nó đơn giản hóa lý do đó.

Vì vậy, nếu converttạo ra kết quả không chính xác từ một số đối số và readFilecung cấp cho nó một đối số như vậy, chúng ta không thấy đó là lỗi do nhà nước đưa ra . Đó là một lỗi trong một chức năng thuần túy!


Tôi nghĩ rằng đây là mô tả tốt nhất (mặc dù những người khác cũng giúp làm rõ mọi thứ cho tôi), cảm ơn.
Stu Cox

Có đáng lưu ý rằng việc sử dụng các đơn vị trong python có thể có ít lợi ích hơn vì python chỉ có một loại (tĩnh), và do đó sẽ không đảm bảo về bất cứ điều gì?
jk.

7

Thật khó để chắc chắn chính xác những gì bạn có nghĩa là "hoàn toàn học tập", nhưng tôi nghĩ câu trả lời chủ yếu là "không".

Như đã giải thích trong Giải quyết đội lúng túng của Simon Peyton Jones ( rất khuyến khích đọc!), I / O đơn điệu có nghĩa là để giải quyết các vấn đề thực sự với cách Haskell sử dụng để xử lý I / O. Đọc ví dụ về máy chủ có Yêu cầu và Phản hồi mà tôi sẽ không sao chép ở đây; nó rất hướng dẫn.

Haskell, không giống như Python, khuyến khích một kiểu tính toán "thuần túy" có thể được thi hành bởi hệ thống loại của nó. Tất nhiên, bạn có thể sử dụng tính tự giác khi lập trình bằng Python để tuân thủ phong cách này, nhưng còn các mô-đun bạn không viết thì sao? Không có nhiều trợ giúp từ hệ thống loại (và các thư viện phổ biến), I / O đơn âm có lẽ ít hữu ích hơn trong Python. Các triết lý của ngôn ngữ chỉ không có nghĩa là để thực thi một sự tách biệt tinh khiết / không tinh khiết nghiêm ngặt.

Lưu ý rằng điều này nói nhiều hơn về các triết lý khác nhau của Haskell và Python hơn là về cách thức I / O đơn điệu hàn lâm. Tôi sẽ không sử dụng nó cho Python.

Một thứ khác. Bạn nói:

Về cơ bản, toàn bộ chương trình sẽ được chứa trong đơn vị IO.

Đúng là mainchức năng Haskell "sống" IO, nhưng các chương trình Haskell thực sự được khuyến khích không sử dụng IObất cứ khi nào không cần thiết. Hầu như mọi chức năng bạn viết không cần thực hiện I / O đều không nên gõ IO.

Vì vậy, tôi muốn nói trong ví dụ cuối cùng của bạn, bạn đã hiểu ngược: mainlà không tinh khiết (vì nó đọc và ghi tệp) nhưng các chức năng cốt lõi như convertlà thuần túy.


3

Tại sao IO không tinh khiết? Bởi vì nó có thể trả về các giá trị khác nhau tại các thời điểm khác nhau. Có một sự phụ thuộc vào thời gian phải được tính, bằng cách này hay cách khác. Điều này thậm chí còn quan trọng hơn với đánh giá lười biếng. Hãy xem xét chương trình sau:

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

Nếu không có đơn vị IO, tại sao dấu nhắc đầu tiên sẽ nhận được đầu ra? Không có gì phụ thuộc vào nó, vì vậy đánh giá lười biếng có nghĩa là nó sẽ không bao giờ được yêu cầu. Cũng không có gì bắt buộc lời nhắc là đầu ra trước khi đầu vào được đọc. Theo như máy tính có liên quan, không có đơn vị IO, hai biểu thức đầu tiên đó hoàn toàn độc lập với nhau. May mắn thay, nameáp đặt một đơn đặt hàng trên hai thứ hai.

Có nhiều cách khác để giải quyết vấn đề phụ thuộc đơn hàng, nhưng sử dụng đơn vị IO có lẽ là cách đơn giản nhất (theo quan điểm ngôn ngữ ít nhất) để cho phép mọi thứ ở trong cõi chức năng lười biếng, không có các phần nhỏ của mã mệnh lệnh. Nó cũng linh hoạt nhất. Ví dụ, bạn có thể dễ dàng xây dựng một đường ống IO một cách linh hoạt khi chạy dựa trên đầu vào của người dùng.


2

Sự hiểu biết (hạn chế) của tôi về lập trình chức năng là các tác dụng phụ / trạng thái nên được giảm thiểu và tách biệt khỏi logic phi trạng thái.

Đó không chỉ là lập trình chức năng; đó thường là một ý tưởng tốt trong bất kỳ ngôn ngữ. Nếu bạn làm kiểm tra đơn vị, cách bạn chia tay nhau read_file(), convert()write_file()nói một cách hoàn hảo một cách tự nhiên bởi vì, mặc dù convert()là đến nay là phần phức tạp nhất và lớn nhất của mã này, viết bài kiểm tra cho nó là tương đối đơn giản: tất cả các bạn cần phải thiết lập là tham số đầu vào . Viết bài kiểm tra read_file()write_file()khó hơn một chút (mặc dù bản thân các chức năng gần như không đáng kể) vì bạn cần tạo và / hoặc đọc mọi thứ trên hệ thống tệp trước và sau khi gọi hàm. Lý tưởng nhất là bạn làm cho các chức năng đó đơn giản đến mức bạn cảm thấy thoải mái khi không thử nghiệm chúng và do đó tiết kiệm cho mình rất nhiều rắc rối.

Sự khác biệt giữa Python và Haskell ở đây là Haskell có trình kiểm tra loại có thể chứng minh rằng các chức năng không có tác dụng phụ. Trong Python, bạn cần hy vọng rằng không ai vô tình bỏ chức năng đọc hoặc ghi tệp vào convert()(giả sử read_config_file()). Trong Haskell khi bạn khai báo convert :: String -> Stringhoặc tương tự, không có IOđơn nguyên, trình kiểm tra loại sẽ đảm bảo rằng đây là một hàm thuần túy chỉ dựa vào tham số đầu vào của nó và không có gì khác. Nếu ai đó cố gắng sửa đổi convertđể đọc tệp cấu hình, họ sẽ nhanh chóng thấy các lỗi trình biên dịch cho thấy rằng họ sẽ phá vỡ độ tinh khiết của hàm. (Và hy vọng họ đủ tỉnh táo để rời read_config_filekhỏi convertvà chuyển kết quả của nó vào convert, duy trì sự thuần khiết.)

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.