Tại sao Haskell (đôi khi) được gọi là “Ngôn ngữ mệnh lệnh tốt nhất”?


83

(Tôi hy vọng câu hỏi này đúng chủ đề - Tôi đã cố gắng tìm kiếm câu trả lời nhưng không tìm thấy câu trả lời chính xác. Nếu câu hỏi này lạc đề hoặc đã được trả lời, vui lòng kiểm duyệt / xóa nó.)

Tôi nhớ mình đã nghe / đọc bình luận nửa đùa nửa thật về việc Haskell là ngôn ngữ mệnh lệnh tốt nhất một vài lần, điều này tất nhiên nghe có vẻ kỳ lạ vì Haskell thường được biết đến nhiều nhất với các tính năng chức năng của nó .

Vì vậy, câu hỏi của tôi là, những phẩm chất / tính năng nào (nếu có) của Haskell đưa ra lý do để biện minh cho việc Haskell được coi là ngôn ngữ mệnh lệnh tốt nhất - hay nó thực sự là một trò đùa?


3
donsbot.wordpress.com/2007/03/10/… <- Dấu chấm phẩy có thể lập trình được.
vivian

10
Trích dẫn này có lẽ bắt nguồn từ phần cuối của Phần giới thiệu Giải quyết đội ngũ khó xử: đầu vào / đầu ra đơn nguyên, đồng thời, ngoại lệ và các lệnh gọi bằng tiếng nước ngoài trong Haskell có nội dung "Nói tóm lại, Haskell là ngôn ngữ lập trình mệnh lệnh lớn nhất thế giới."
Russell O'Connor,

@Russel: cảm ơn vì đã chỉ ra nguồn gốc có thể xảy ra nhất (có vẻ như chính SPJ) của câu nói đó!
hvr

bạn có thể làm nghiêm ngặt mệnh lệnh OO với Haskell: ekmett / struct
Janus Troelsen

Câu trả lời:


90

Tôi coi đó là một nửa sự thật. Haskell có một khả năng trừu tượng đáng kinh ngạc, bao gồm cả việc trừu tượng hóa những ý tưởng bắt buộc. Ví dụ, Haskell không có vòng lặp while mệnh lệnh tích hợp, nhưng chúng ta có thể viết nó và bây giờ nó làm được:

while :: (Monad m) => m Bool -> m () -> m ()
while cond action = do
    c <- cond
    if c 
        then action >> while cond action
        else return ()

Mức độ trừu tượng này là khó đối với nhiều ngôn ngữ mệnh lệnh. Điều này có thể được thực hiện trong các ngôn ngữ mệnh lệnh có các dấu đóng; ví dụ. Python và C #.

Nhưng Haskell cũng có khả năng (rất độc đáo) để mô tả các tác dụng phụ được phép , bằng cách sử dụng các lớp Monad. Ví dụ, nếu chúng ta có một hàm:

foo :: (MonadWriter [String] m) => m Int

Đây có thể là một hàm "mệnh lệnh", nhưng chúng tôi biết rằng nó chỉ có thể thực hiện hai việc:

  • "Xuất" một luồng chuỗi
  • trả lại một Int

Nó không thể in ra bảng điều khiển hoặc thiết lập kết nối mạng, v.v. Kết hợp với khả năng trừu tượng, bạn có thể viết các hàm hoạt động trên "bất kỳ tính toán nào tạo ra một luồng", v.v.

Đó thực sự là tất cả về khả năng trừu tượng của Haskell khiến nó trở thành một ngôn ngữ mệnh lệnh rất tốt.

Tuy nhiên, nửa sai là cú pháp. Tôi thấy Haskell khá dài dòng và khó sử dụng theo phong cách mệnh lệnh. Dưới đây là một ví dụ về tính toán kiểu mệnh lệnh bằng cách sử dụng whilevòng lặp trên , vòng lặp này tìm phần tử cuối cùng của danh sách được liên kết:

lastElt :: [a] -> IO a
lastElt [] = fail "Empty list!!"
lastElt xs = do
    lst <- newIORef xs
    ret <- newIORef (head xs)
    while (not . null <$> readIORef lst) $ do
        (x:xs) <- readIORef lst
        writeIORef lst xs
        writeIORef ret x
    readIORef ret

Tất cả những gì IORef rác, việc đọc kép, phải ràng buộc kết quả của một lần đọc, fmapping ( <$>) để hoạt động trên kết quả của một phép tính nội tuyến ... tất cả đều trông rất phức tạp. Nó có rất nhiều ý nghĩa từ góc độ chức năng , nhưng các ngôn ngữ mệnh lệnh có xu hướng quét hầu hết các chi tiết này xuống dưới tấm thảm để làm cho chúng dễ sử dụng hơn.

Phải thừa nhận rằng, có lẽ nếu chúng ta sử dụng bộ whiletổ hợp kiểu khác thì nó sẽ sạch hơn. Nhưng nếu bạn đưa triết lý đó đi đủ xa (sử dụng một bộ tổ hợp phong phú để thể hiện bản thân một cách rõ ràng), thì bạn lại đến với lập trình chức năng. Haskell kiểu mệnh lệnh không "chảy" như một ngôn ngữ mệnh lệnh được thiết kế tốt, ví dụ như python.

Tóm lại, với sự nâng cao về mặt cú pháp, Haskell có thể là ngôn ngữ mệnh lệnh tốt nhất. Nhưng, về bản chất của nâng cơ mặt, nó sẽ thay thế một thứ đẹp đẽ bên trong và thật bằng một thứ gì đó đẹp đẽ và giả tạo bên ngoài.

CHỈNH SỬA : Tương phản lastEltvới chuyển ngữ của con trăn này:

def last_elt(xs):
    assert xs, "Empty list!!"
    lst = xs
    ret = xs.head
    while lst:
        ret = lst.head
        lst = lst.tail
    return ret 

Cùng một số dòng, nhưng mỗi dòng có tiếng ồn ít hơn một chút.


CHỈNH SỬA 2

Đối với những gì nó đáng giá, đây là cách một sự thay thế thuần túy trong Haskell trông như thế nào:

lastElt = return . last

Đó là nó. Hoặc, nếu bạn cấm tôi sử dụng Prelude.last:

lastElt [] = fail "Unsafe lastElt called on empty list"
lastElt [x] = return x
lastElt (_:xs) = lastElt xs

Hoặc, nếu bạn muốn nó hoạt động trên bất kỳ Foldablecấu trúc dữ liệu nào và nhận ra rằng bạn không thực sự cần IO xử lý lỗi:

import Data.Foldable (Foldable, foldMap)
import Data.Monoid (Monoid(..), Last(..))

lastElt :: (Foldable t) => t a -> Maybe a
lastElt = getLast . foldMap (Last . Just)

với Map, ví dụ:

λ➔ let example = fromList [(10, "spam"), (50, "eggs"), (20, "ham")] :: Map Int String
λ➔ lastElt example
Just "eggs"

Các (.)nhà điều hành là hàm hợp .


2
Bạn có thể làm cho tiếng ồn IORef ít gây khó chịu hơn bằng cách tạo ra nhiều nội dung trừu tượng hơn.
augustss

1
@augustss, hmm, tôi tò mò về điều đó. Ý của bạn là nhiều trừu tượng cấp miền hơn hay chỉ bằng cách xây dựng một ngôn ngữ con mệnh lệnh phong phú hơn ". Đối với điều đầu tiên, tôi đồng ý - nhưng tâm trí của tôi liên kết lập trình mệnh lệnh với tính trừu tượng thấp (giả thuyết hoạt động của tôi là khi tính trừu tượng tăng lên, kiểu hội tụ về chức năng). Đối với phần sau, tôi thực sự muốn xem ý của bạn là gì, bởi vì tôi không thể nghĩ ra cách đầu của mình.
luqui

2
@luqui Sử dụng ST sẽ là một ví dụ điển hình để xác định các tác dụng phụ được phép. Như một phần thưởng, bạn có thể quay trở lại một phép tính thuần túy từ ST.
fuz

4
Sử dụng Python để so sánh là không hoàn toàn công bằng - như bạn nói, nó được thiết kế tốt, một trong những ngôn ngữ mệnh lệnh rõ ràng nhất về mặt cú pháp mà tôi quen thuộc. Sự so sánh tương tự sẽ cho rằng hầu hết các ngôn ngữ mệnh lệnh đều khó sử dụng trong văn phong mệnh lệnh ... tuy nhiên, có lẽ đó chính xác là ý của bạn. ;]
CA McCann

5
Chú thích cuối cùng của cuộc trò chuyện, dành cho hậu thế: @augustss sử dụng tính đa hình đặc biệt để IORefẩn ý , hoặc ít nhất là cố gắng và bị cản trở bởi những thay đổi đối với GHC. : [
CA McCann

22

Đó không phải là một trò đùa, và tôi tin điều đó. Tôi sẽ cố gắng giữ cho điều này có thể truy cập được cho những người không biết bất kỳ Haskell nào. Haskell sử dụng ký hiệu do-notation (trong số những thứ khác) để cho phép bạn viết mã mệnh lệnh (vâng, nó sử dụng monads, nhưng đừng lo lắng về điều đó). Dưới đây là một số lợi thế mà Haskell mang lại cho bạn:

  • Dễ dàng tạo các chương trình con. Giả sử rằng tôi muốn một hàm in giá trị ra stdout và stderr. Tôi có thể viết như sau, xác định chương trình con bằng một dòng ngắn:

    do let printBoth s = putStrLn s >> hPutStrLn stderr s
       printBoth "Hello"
       -- Some other code
       printBoth "Goodbye"
    
  • Dễ dàng vượt qua mã xung quanh. Vì tôi đã viết ở trên, nếu bây giờ tôi muốn sử dụng printBothhàm để in ra tất cả danh sách các chuỗi, điều đó dễ dàng thực hiện bằng cách chuyển chương trình con của tôi đến mapM_hàm:

    mapM_ printBoth ["Hello", "World!"]
    

    Một ví dụ khác, mặc dù không bắt buộc, là sắp xếp. Giả sử bạn chỉ muốn sắp xếp các chuỗi theo độ dài. Bạn có thể viết:

    sortBy (\a b -> compare (length a) (length b)) ["aaaa", "b", "cc"]
    

    Mà sẽ cung cấp cho bạn ["b", "cc", "aaaa"]. (Bạn cũng có thể viết nó ngắn hơn thế, nhưng đừng bận tâm vào lúc này.)

  • Dễ dàng sử dụng lại mã. mapM_Hàm đó được sử dụng rất nhiều và thay thế cho từng vòng lặp trong các ngôn ngữ khác. Ngoài ra còn cóforever có chức năng hoạt động giống như một thời gian (true) và nhiều chức năng khác có thể được chuyển mã và thực thi nó theo những cách khác nhau. Vì vậy, các vòng lặp trong các ngôn ngữ khác được thay thế bằng các chức năng điều khiển này trong Haskell (không phải là đặc biệt - bạn có thể tự định nghĩa chúng rất dễ dàng). Nói chung, điều này làm cho điều kiện lặp khó bị sai, giống như vòng lặp for-each khó bị sai hơn so với các vòng lặp tương đương dài tay (ví dụ trong Java) hoặc các vòng lập chỉ mục mảng (ví dụ: trong C).

  • Ràng buộc không phải là chuyển nhượng. Về cơ bản, bạn chỉ có thể gán cho một biến một lần (thay vì gán tĩnh đơn lẻ). Điều này loại bỏ nhiều nhầm lẫn về các giá trị có thể có của một biến tại bất kỳ điểm nào đã cho (giá trị của nó chỉ được đặt trên một dòng).
  • Có tác dụng phụ. Giả sử rằng tôi muốn đọc một dòng từ stdin và viết nó trên stdout sau khi áp dụng một số hàm cho nó (chúng ta sẽ gọi nó là foo). Bạn có thể viết:

    do line <- getLine
       putStrLn (foo line)
    

    Tôi biết ngay rằng điều foođó không có bất kỳ tác dụng phụ không mong muốn nào (như cập nhật một biến toàn cục, hoặc phân bổ bộ nhớ, hoặc bất cứ thứ gì), bởi vì kiểu của nó phải là String -> String, có nghĩa là nó là một hàm thuần túy; bất kỳ giá trị nào tôi vượt qua, nó phải trả về cùng một kết quả mọi lúc, không có tác dụng phụ. Haskell tách biệt mã hiệu quả phụ khỏi mã thuần túy. Trong một cái gì đó như C, hoặc thậm chí là Java, điều này không rõ ràng (phương thức getFoo () đó có thay đổi trạng thái không? Bạn hy vọng là không, nhưng nó có thể làm được ...).

  • Thu gom rác thải. Ngày nay, rất nhiều ngôn ngữ được thu thập, nhưng điều đáng nói: không có gì phức tạp trong việc phân bổ và phân bổ bộ nhớ.

Có thể có thêm một số lợi thế bên cạnh, nhưng đó là những lợi thế mà bạn cần lưu ý.


9
Tôi sẽ thêm vào loại an toàn mạnh mẽ. Haskell cho phép trình biên dịch loại bỏ một lượng lớn lỗi. Sau khi làm việc trên một số mã Java gần đây, tôi đã được nhắc nhở rằng con trỏ null khủng khiếp như thế nào và thiếu bao nhiêu OOP nếu không có kiểu tính tổng.
Michael Snoyman

1
Cảm ơn vì công phu của bạn! Những lợi thế được đề cập của bạn dường như chỉ dừng lại ở việc Haskell coi các hiệu ứng '' mệnh lệnh '' như một đối tượng hạng nhất (do đó có thể kết hợp được) cùng với khả năng '' chứa '' những hiệu ứng đó trong một phạm vi được phân định. Đây có phải là một bản tóm tắt được nén đầy đủ không?
hvr

19
@Michael Snoyman: Nhưng kiểu tính tổng rất dễ dàng trong OOP! Chỉ cần xác định một lớp trừu tượng đại diện cho mã hóa Church của kiểu dữ liệu của bạn, các lớp con cho các trường hợp, các giao diện cho các lớp có thể xử lý từng trường hợp và sau đó chuyển các đối tượng hỗ trợ từng giao diện đến một đối tượng sum, sử dụng đa hình kiểu con cho luồng điều khiển (như bạn nên). Không thể đơn giản hơn. Tại sao bạn ghét các mẫu thiết kế?
CA McCann

9
@camccann Tôi biết bạn đang nói đùa, nhưng về cơ bản đó là những gì tôi đã thực hiện trong dự án của mình.
Michael Snoyman

9
@Michael Snoyman: Vậy thì lựa chọn tốt! Trò đùa thực sự là tôi đang mô tả những gì khá nhiều là bảng mã tốt nhất theo cách nghe như một trò đùa. Ha ha! Cười suốt quãng đường đến giá treo cổ ...
CA McCann

16

Ngoài những gì người khác đã đề cập, việc có những hành động mang lại hiệu quả phụ đôi khi hữu ích. Đây là một ví dụ ngớ ngẩn cho thấy ý tưởng:

f = sequence_ (reverse [print 1, print 2, print 3])

Ví dụ này cho thấy cách bạn có thể xây dựng các tính toán với các hiệu ứng phụ (trong ví dụ này print) và sau đó đưa cấu trúc dữ liệu vào hoặc thao tác chúng theo những cách khác, trước khi thực sự thực thi chúng.


Tôi nghĩ rằng các mã javascript rằng tương ứng với điều này sẽ là: call = x => x(); sequence_ = xs => xs.forEach(call) ;print = console.log; f = () => sequence_([()=> print(1), () => print(2), () => print(3)].reverse()). Sự khác biệt chính mà tôi thấy là chúng tôi cần thêm một vài thứ () =>.
Hjulle
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.