Tại sao `head` của Haskell lại gặp sự cố trên một danh sách trống (hoặc tại sao * không * nó trả về một danh sách trống)? (Ngôn ngữ triết học)


76

Lưu ý cho những người đóng góp tiềm năng khác: Vui lòng sử dụng ký hiệu trừu tượng hoặc toán học để đưa ra quan điểm của bạn. Nếu tôi thấy câu trả lời của bạn không rõ ràng, tôi sẽ yêu cầu làm sáng tỏ, nhưng nếu không, hãy thoải mái thể hiện bản thân một cách thoải mái.

Nói rõ hơn: Tôi không tìm kiếm một "két an toàn" head, cũng không phải là sự lựa chọn headđặc biệt có ý nghĩa đặc biệt. Phần chính của câu hỏi theo sau cuộc thảo luận về headhead', dùng để cung cấp ngữ cảnh.

Tôi đã hack Haskell được vài tháng rồi (đến mức nó đã trở thành ngôn ngữ chính của tôi), nhưng phải thừa nhận rằng tôi không được thông tin đầy đủ về một số khái niệm nâng cao hơn cũng như chi tiết về triết lý của ngôn ngữ (mặc dù Tôi sẵn sàng học hỏi). Câu hỏi của tôi sau đó không phải là vấn đề kỹ thuật quá nhiều (trừ khi nó đúng và tôi không nhận ra nó) vì nó là một trong những triết học.

Đối với ví dụ này, tôi đang nói đến head.

Như tôi tưởng tượng bạn sẽ biết,

Prelude> head []    
*** Exception: Prelude.head: empty list

Điều này theo sau từ head :: [a] -> a. Đủ công bằng. Rõ ràng là người ta không thể trả về một phần tử không thuộc loại (vẫy tay). Nhưng đồng thời, nó rất đơn giản (nếu không muốn nói là tầm thường) để xác định

head' :: [a] -> Maybe a
head' []     = Nothing
head' (x:xs) = Just x

Tôi đã thấy một số thảo luận nhỏ về điều này ở đây trong phần bình luận của một số tuyên bố nhất định. Đáng chú ý, một Alex Stangl nói

'Có những lý do chính đáng để không làm cho mọi thứ trở nên "an toàn" và đưa ra các ngoại lệ khi các điều kiện tiên quyết bị vi phạm.'

Tôi không nhất thiết phải thắc mắc về khẳng định này, nhưng tôi tò mò không biết những "lý do chính đáng" này là gì.

Ngoài ra, Paul Johnson nói,

'Ví dụ: bạn có thể xác định "safeHead :: [a] -> Có thể là", nhưng bây giờ thay vì xử lý một danh sách trống hoặc chứng minh điều đó không thể xảy ra, bạn phải xử lý "Không có gì" hoặc chứng minh điều đó không thể xảy ra . '

Giọng điệu mà tôi đọc được từ nhận xét đó cho thấy rằng đây là một sự gia tăng đáng kể về độ khó / độ phức tạp / điều gì đó, nhưng tôi không chắc rằng mình nắm được những gì anh ấy đang đưa ra ở đó.

Một Steven Pruzina nói (năm 2011, không kém),

"Có một lý do sâu xa hơn tại sao ví dụ: 'head' không thể chống được sự cố. Để trở nên đa hình nhưng vẫn xử lý một danh sách trống, 'head' phải luôn trả về một biến kiểu không có trong bất kỳ danh sách trống cụ thể nào. Nó sẽ là Thật tuyệt nếu Haskell làm được điều đó ... ”.

Tính đa hình có bị mất khi cho phép xử lý danh sách trống không? Nếu vậy, làm thế nào và tại sao? Có trường hợp cụ thể nào làm cho điều này rõ ràng không? Phần này được trả lời bởi @Russell O'Connor. Tất nhiên, bất kỳ suy nghĩ nào khác đều được đánh giá cao.

Tôi sẽ chỉnh sửa điều này như sự rõ ràng và đề xuất ra lệnh. Bất kỳ suy nghĩ, giấy tờ, vv, bạn có thể cung cấp sẽ được đánh giá cao nhất.


'Head' không phải là không an toàn (nó là một phần, nó không phải là toàn bộ) cũng như không ném ra một ngoại lệ (nó không được xác định cho một danh sách trống).
Lemming

Câu trả lời:


103

Tính đa hình có bị mất khi cho phép xử lý danh sách trống không? Nếu vậy, làm thế nào và tại sao? Có trường hợp cụ thể nào làm cho điều này rõ ràng không?

Định lý miễn phí cho headcác phát biểu rằng

f . head = head . $map f

Áp dụng định lý này để []ngụ ý rằng

f (head []) = head (map f []) = head []

Định lý này phải phù hợp với mọi f, vì vậy đặc biệt nó phải phù hợp với const Trueconst False. Điều này nghĩa là

True = const True (head []) = head [] = const False (head []) = False

Vì vậy, nếu headlà đa hình đúng và head []là một giá trị tổng, thì Truesẽ bằng False.

Tái bút. Tôi có một số nhận xét khác về bối cảnh cho câu hỏi của bạn vì nếu bạn có điều kiện tiên quyết là danh sách của bạn không trống thì bạn nên thực thi nó bằng cách sử dụng loại danh sách không trống trong chữ ký hàm của bạn thay vì sử dụng danh sách.


1
Đó chỉ là loại câu trả lời mà tôi đang tìm kiếm. Tôi mong nhận được những nhận xét khác của bạn, nếu bạn quan tâm. :)
Jack Henahan

2
Và tôi vừa tra cứu Định lý Wadler Miễn phí! để đọc thêm về chủ đề của các định lý miễn phí.
Jack Henahan

21
Tôi không hiểu tại sao điều này lại gây ra vấn đề với head :: [a] -> Maybe a, trong đó có một định lý miễn phí fmap f . head = head . map fvà do đó bạn chỉ cần hiểu Nothing == Nothing. Trừ khi bạn chỉ đang cố gắng chỉ ra lý do tại sao không thể có một default :: forall a . agiá trị nào đó có thể được trả về head []và chúng ta có thể so khớp theo mẫu ... nhưng có nhiều cách đơn giản hơn để nói về điều đó.
J. Abrahamson,

@ J.Abrahamson cf Data.List.Safe
bjd2385 21/12/17

25

Tại sao mọi người lại sử dụng head :: [a] -> athay vì kết hợp mẫu? Một trong những lý do là vì bạn biết rằng đối số không được để trống và không muốn viết mã để xử lý trường hợp đối số trống.

Tất nhiên, head'kiểu của bạn [a] -> Maybe ađược xác định trong thư viện chuẩn là Data.Maybe.listToMaybe. Nhưng nếu bạn thay thế việc sử dụng headvới listToMaybe, bạn phải viết mã để xử lý trường hợp trống, điều này làm hỏng mục đích sử dụng này head.

Tôi không nói rằng sử dụng headlà một phong cách tốt. Nó che giấu thực tế rằng nó có thể dẫn đến một ngoại lệ, và theo nghĩa này, nó không tốt. Nhưng nó đôi khi là thuận tiện. Vấn đề là headphục vụ một số mục đích mà không thể được phục vụ bởi listToMaybe.

Dấu ngoặc kép cuối cùng trong câu hỏi (về tính đa hình) chỉ đơn giản có nghĩa là không thể xác định một hàm kiểu [a] -> atrả về giá trị trong danh sách trống (như Russell O'Connor đã giải thích trong câu trả lời của mình ).


8

Thật tự nhiên khi mong đợi những điều sau sẽ được giữ: xs === head xs : tail xs- một danh sách giống hệt với phần tử đầu tiên của nó, tiếp theo là phần còn lại. Có vẻ hợp lý, phải không?

Bây giờ, hãy đếm số khuyết điểm (ứng dụng của :), bỏ qua các yếu tố thực tế, khi áp dụng 'luật' có mục đích []: []phải giống với foo : bar, nhưng cái trước có 0 khuyết điểm, trong khi cái sau có (ít nhất) một. Ồ, có gì đó không ổn ở đây!

Hệ thống kiểu của Haskell, đối với tất cả các điểm mạnh của nó, không thể hiện thực tế là bạn chỉ nên gọi headtrên một danh sách không trống (và 'luật' chỉ có hiệu lực cho danh sách không trống). Sử dụnghead sẽ thay đổi gánh nặng bằng chứng cho lập trình viên, người sẽ đảm bảo rằng nó không được sử dụng trong danh sách trống. Tôi tin rằng các ngôn ngữ được đánh máy phụ thuộc như Agda có thể giúp ích ở đây.

Cuối cùng, một mô tả triết học-hoạt động hơn một chút: nên head ([] :: [a]) :: athực hiện như thế nào? Việc liên kết một giá trị kiểu angoài không khí loãng là không thể (hãy nghĩ đến các kiểu không có người ở chẳng hạn data Falsum), và sẽ chứng minh được bất cứ điều gì (thông qua phép đẳng cấu Curry-Howard).


4
"có thể chứng minh bất cứ điều gì (thông qua thuyết đẳng cấu Curry-Howard)" Đó là một điểm rất thú vị. Tôi đã không xem xét một kết nối như vậy. Tốt lắm.
Jack Henahan

4

Có một số cách khác nhau để nghĩ về điều này. Vì vậy, tôi sẽ tranh luận cả ủng hộ và chống lại head':

Chống lại head':

Không cần phải có head': Vì danh sách là một kiểu dữ liệu cụ thể, mọi thứ bạn có thể làm với head'bạn đều có thể thực hiện bằng cách đối sánh mẫu.

Hơn nữa, với việc head'bạn chỉ đang đánh đổi một cỗ máy này cho một cỗ máy khác. Tại một số thời điểm, bạn muốn bắt đầu bằng đồng thau và hoàn thành một số công việc trên phần tử danh sách cơ bản.

Để bảo vệ head':

Nhưng kết hợp mẫu che khuất những gì đang xảy ra. Trong Haskell, chúng tôi quan tâm đến việc tính toán các hàm, điều này được hoàn thành tốt hơn bằng cách viết chúng theo phong cách không có điểm bằng cách sử dụng các thành phần và tổ hợp.

Hơn nữa, suy nghĩ về các []Maybefunctors, head'cho phép bạn di chuyển qua lại giữa chúng (Đặc biệt là Applicativeví dụ của []with pure = replicate.)


Mặc dù, theo một số logic cho thấy rằng không cần phải có headgì cả, nhưng nó vẫn tồn tại. Nhìn chung, tôi thích câu trả lời của bạn hơn và tôi muốn quan tâm đến bất kỳ suy nghĩ nào khác của bạn về những điểm bạn đã đưa ra.
Jack Henahan

Thực tế mà nói, có những lúc tôi muốn headvà những nơi khác tôi ước tôi có (hoặc tôi chỉ định nghĩa) head'.
MtnViewMark

3
@MtnViewMark Nếu bạn đang muốn head', bạn không cần tìm đâu xa hơn Data.Maybe.listToMaybe.
Russell O'Connor,

Có lẽ head'' :: MonadPlus m => [a] -> m asẽ hữu ích hơn.
messmasttter

3

Nếu trong trường hợp sử dụng của bạn, một danh sách trống không có ý nghĩa gì, bạn luôn có thể chọn sử dụng NonEmptythay thế, nơi neHeadan toàn để sử dụng. Nếu bạn nhìn nó từ góc độ đó, thì đó không phải là headhàm không an toàn, mà là toàn bộ cấu trúc dữ liệu danh sách (một lần nữa, cho trường hợp sử dụng đó).


1

Tôi nghĩ đây là một vấn đề của sự đơn giản và đẹp. Đó là, tất nhiên, trong mắt của người xem.

Nếu đến từ nền Lisp, bạn có thể biết rằng danh sách được xây dựng từ các ô khuyết điểm, mỗi ô có một phần tử dữ liệu và một con trỏ đến ô tiếp theo. Danh sách trống không phải là một danh sách, mà là một ký hiệu đặc biệt. Và Haskell đi theo lý luận này.

Theo quan điểm của tôi, nó vừa gọn gàng hơn, đơn giản hơn để lý luận và truyền thống hơn, nếu danh sách trống và danh sách là hai thứ khác nhau.

... Tôi có thể nói thêm - nếu bạn lo lắng về việc đầu không an toàn - đừng sử dụng nó, thay vào đó hãy sử dụng khớp mẫu:

sum     [] = 0
sum (x:xs) = x + sum xs

2
Ồ, chắc chắn, tôi luôn thích kết hợp mẫu hơn head. Đây là một sự tò mò hơn là một mối quan tâm. Re phần còn lại của nhận xét của bạn: Truyền thống theo nghĩa nào? Truyền thống của ai? Tại sao nó là truyền thống?
Jack Henahan

Vâng, truyền thống của lập trình chức năng. Tôi có thể phải ăn hết các từ của mình ngay lập tức - trong Lisp, (car '()) trả về NIL. Nhưng Haskell là một nỗ lực để thống nhất đám đông lập trình chức năng hiện đại. Và cả OCaml và Miranda (tôi tin rằng) đều mắc lỗi khi làm việc trong danh sách trống.
Johan Kotlinski

Và đó là sự tò mò lịch sử mà tôi đang cố gắng hợp lý hóa. Roughly ( cực kỳ đại khái), []tương đương với tập hợp rỗng. Trong ngữ cảnh toán học, tập hợp rỗng đơn giản tập hợp không có gì trong đó. Tương tự, []vẫn là một danh sách, và trên thực tế phải danh sách không có phần tử (và có thể không có kiểu). Nếu đây (ví dụ) là một giới hạn của phép tính lambda đã nhập, điều đó có thể giúp giải thích cách xử lý [], nhưng tôi không chắc đây là trường hợp.
Jack Henahan
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.