Từ khóa `forall` trong Haskell / GHC làm gì?


312

Tôi bắt đầu hiểu cách foralltừ khóa được sử dụng trong cái gọi là "loại tồn tại" như thế này:

data ShowBox = forall s. Show s => SB s

Đây chỉ là một tập hợp con, tuy nhiên, về cách forallsử dụng và tôi chỉ đơn giản là không thể bao bọc tâm trí của mình xung quanh việc sử dụng nó trong những thứ như thế này:

runST :: forall a. (forall s. ST s a) -> a

Hoặc giải thích tại sao những điều này là khác nhau:

foo :: (forall a. a -> a) -> (Char, Bool)
bar :: forall a. ((a -> a) -> (Char, Bool))

Hoặc toàn bộ RankNTypes...

Tôi có xu hướng thích tiếng Anh rõ ràng, không có biệt ngữ hơn là các loại ngôn ngữ bình thường trong môi trường học thuật. Hầu hết các giải thích tôi cố gắng đọc về điều này (những giải thích tôi có thể tìm thấy thông qua các công cụ tìm kiếm) có những vấn đề sau:

  1. Chúng chưa hoàn chỉnh. Họ giải thích một phần của việc sử dụng từ khóa này (như "các loại hiện sinh") khiến tôi cảm thấy hạnh phúc cho đến khi tôi đọc mã sử dụng nó theo một cách hoàn toàn khác (như runST, foobarở trên).
  2. Chúng dày đặc với các giả định mà tôi đã đọc mới nhất trong bất kỳ nhánh nào của toán học rời rạc, lý thuyết thể loại hoặc đại số trừu tượng là phổ biến trong tuần này. (Nếu tôi không bao giờ đọc các từ "tham khảo bài viết bất cứ điều gì để biết chi tiết thực hiện" một lần nữa, nó sẽ quá sớm.)
  3. Chúng được viết theo những cách thường xuyên biến những khái niệm đơn giản thành ngữ pháp và ngữ nghĩa bị xoắn và gãy.

Vì thế...

Về câu hỏi thực tế. Bất cứ ai cũng có thể giải thích hoàn toàn foralltừ khóa bằng tiếng Anh rõ ràng, đơn giản (hoặc, nếu nó tồn tại ở đâu đó, chỉ ra một lời giải thích rõ ràng mà tôi đã bỏ qua) mà không cho rằng tôi là một nhà toán học chìm trong thuật ngữ?


Chỉnh sửa để thêm:

Có hai câu trả lời nổi bật từ những câu trả lời chất lượng cao hơn bên dưới, nhưng thật không may, tôi chỉ có thể chọn một câu trả lời là tốt nhất. Câu trả lời của Norman rất chi tiết và hữu ích, giải thích mọi thứ theo cách cho thấy một số nền tảng lý thuyết forallvà đồng thời cho tôi thấy một số ý nghĩa thực tế của nó. câu trả lời của yairchubao phủ một khu vực không ai khác đề cập (biến loại trong phạm vi) và minh họa tất cả các khái niệm bằng mã và phiên GHCi. Tôi có thể chọn cả hai là tốt nhất, tôi sẽ. Thật không may, tôi không thể và sau khi xem xét kỹ cả hai câu trả lời, tôi đã quyết định rằng yairchu hơi nghiêng về Norman vì mã minh họa và lời giải thích kèm theo. Tuy nhiên, điều này hơi bất công vì thực sự tôi cần cả hai câu trả lời để hiểu điều này đến mức forallkhông để lại cho tôi cảm giác sợ hãi mờ nhạt khi tôi nhìn thấy nó trong một chữ ký loại.


7
Haskell wiki dường như khá thân thiện với người mới bắt đầu về chủ đề này.
jhegedus

Câu trả lời:


263

Hãy bắt đầu với một ví dụ mã:

foob :: forall a b. (b -> b) -> b -> (a -> b) -> Maybe a -> b
foob postProcess onNothin onJust mval =
    postProcess val
    where
        val :: b
        val = maybe onNothin onJust mval

Mã này không biên dịch (lỗi cú pháp) trong Haskell 98. Nó yêu cầu một phần mở rộng để hỗ trợ foralltừ khóa.

Về cơ bản, có 3 cách sử dụng phổ biến khác nhau cho foralltừ khóa (hoặc ít nhất là có vẻ như vậy ) và mỗi cách đều có phần mở rộng Haskell riêng : ScopedTypeVariables, RankNTypes/ Rank2Types, ExistentialQuantification.

Đoạn mã trên không gặp lỗi cú pháp với một trong hai lỗi được kích hoạt, mà chỉ kiểm tra kiểu có ScopedTypeVariablesbật.

Biến loại phạm vi:

Các biến loại có phạm vi giúp người ta chỉ định các loại mã cho wherecác mệnh đề. Nó làm cho btrong val :: bcùng một như btrong foob :: forall a b. (b -> b) -> b -> (a -> b) -> Maybe a -> b.

Một điểm khó hiểu : bạn có thể nghe thấy rằng khi bạn bỏ qua foralltừ một loại, nó thực sự vẫn còn ở đó. ( từ câu trả lời của Norman: "thông thường các ngôn ngữ này bỏ qua forall từ các loại đa hình" ). Khiếu nại này là chính xác, nhưng nó đề cập đến các mục đích sử dụng khác forallvà không liên quan đến việc ScopedTypeVariablessử dụng.

Loại-N:

Hãy bắt đầu với điều đó mayb :: b -> (a -> b) -> Maybe a -> btương đương với mayb :: forall a b. b -> (a -> b) -> Maybe a -> b, ngoại trừ khi ScopedTypeVariablesđược bật.

Điều này có nghĩa là nó hoạt động cho mọi ab.

Hãy nói rằng bạn muốn làm một cái gì đó như thế này.

ghci> let putInList x = [x]
ghci> liftTup putInList (5, "Blah")
([5], ["Blah"])

Điều gì phải là loại này liftTup? Đó là liftTup :: (forall x. x -> f x) -> (a, b) -> (f a, f b). Để xem tại sao, hãy thử mã hóa nó:

ghci> let liftTup liftFunc (a, b) = (liftFunc a, liftFunc b)
ghci> liftTup (\x -> [x]) (5, "Hello")
    No instance for (Num [Char])
    ...
ghci> -- huh?
ghci> :t liftTup
liftTup :: (t -> t1) -> (t, t) -> (t1, t1)

"Hmm .. tại sao GHC suy luận rằng bộ dữ liệu phải chứa hai loại cùng loại? Hãy nói với họ rằng họ không phải là"

-- test.hs
liftTup :: (x -> f x) -> (a, b) -> (f a, f b)
liftTup liftFunc (t, v) = (liftFunc t, liftFunc v)

ghci> :l test.hs
    Couldnt match expected type 'x' against inferred type 'b'
    ...

Hừm. vì vậy đây GHC không cho phép chúng ta áp dụng liftFunctrên vv :: bliftFuncmuốn một x. Chúng tôi thực sự muốn chức năng của chúng tôi để có được một chức năng chấp nhận bất kỳ có thể x!

{-# LANGUAGE RankNTypes #-}
liftTup :: (forall x. x -> f x) -> (a, b) -> (f a, f b)
liftTup liftFunc (t, v) = (liftFunc t, liftFunc v)

Vì vậy, nó không liftTuphoạt động cho tất cả x, đó là chức năng mà nó có được.

Định lượng hiện sinh:

Hãy sử dụng một ví dụ:

-- test.hs
{-# LANGUAGE ExistentialQuantification #-}
data EQList = forall a. EQList [a]
eqListLen :: EQList -> Int
eqListLen (EQList x) = length x

ghci> :l test.hs
ghci> eqListLen $ EQList ["Hello", "World"]
2

Nó khác với loại-N như thế nào?

ghci> :set -XRankNTypes
ghci> length (["Hello", "World"] :: forall a. [a])
    Couldnt match expected type 'a' against inferred type '[Char]'
    ...

Với Loại-N, forall acó nghĩa là biểu thức của bạn phải phù hợp với tất cả các as có thể . Ví dụ:

ghci> length ([] :: forall a. [a])
0

Một danh sách trống không hoạt động như một danh sách của bất kỳ loại nào.

Vì vậy, với Định lượng Hiện sinh, foralls trong các datađịnh nghĩa có nghĩa là, giá trị chứa thể thuộc bất kỳ loại phù hợp nào , không phảitất cả các loại phù hợp.


OK, tôi đã có sáu giờ và bây giờ có thể giải mã câu trả lời của bạn. :) Giữa bạn và Norman tôi nhận được chính xác loại câu trả lời mà tôi đang tìm kiếm. Cảm ơn.
CHỈ CẦN

2
Trên thực tế, bạn làm cho ScopedTypeVariablescó vẻ tồi tệ hơn nó. Nếu bạn viết loại b -> (a -> b) -> Maybe a -> bcó phần mở rộng này trên đó thì nó vẫn sẽ tương đương chính xác forall a b. b -> (a -> b) -> Maybe a -> b. Tuy nhiên, nếu bạn muốn tham khảo cùng b (và không được định lượng ngầm) thì bạn cần phải viết phiên bản được định lượng rõ ràng. Nếu không, STVsẽ là một phần mở rộng cực kỳ xâm nhập.
nomolo

1
@nominolo: Tôi không có ý định hạ bệ ScopedTypeVariables, và tôi không nghĩ rằng nó tệ. imho nó là một công cụ rất hữu ích cho quá trình lập trình, và đặc biệt đối với người mới bắt đầu Haskell, và tôi rất biết ơn vì nó tồn tại.
yairchu

2
Đây là một câu hỏi khá cũ (và câu trả lời), nhưng có thể đáng để cập nhật nó để phản ánh thực tế rằng các loại tồn tại có thể được biểu thị bằng GADT theo cách mà (ít nhất là với tôi) làm cho việc định lượng dễ hiểu hơn nhiều.
dfeuer 7/2/2015

1
Cá nhân tôi nghĩ rằng việc giải thích / hiểu ký hiệu tồn tại theo cách dịch sang dạng GADT dễ dàng hơn so với bản thân nó, nhưng bạn chắc chắn có thể tự do nghĩ khác.
dfeuer 8/2/2015

117

Bất cứ ai cũng có thể giải thích hoàn toàn từ khóa forall bằng tiếng Anh rõ ràng, đơn giản?

Không. (Chà, có lẽ Don Stewart có thể.)

Dưới đây là các rào cản đối với một lời giải thích đơn giản, rõ ràng hoặc forall:

  • Đó là một bộ định lượng. Bạn phải có ít nhất một chút logic (tính toán vị ngữ) để thấy một bộ lượng hóa phổ quát hoặc tồn tại. Nếu bạn chưa bao giờ thấy tính toán vị ngữ hoặc không thoải mái với định lượng (và tôi đã thấy sinh viên trong các kỳ thi tiến sĩ không thoải mái), thì đối với bạn, không có lời giải thích dễ dàng nào forall.

  • Đó là một định lượng loại . Nếu bạn chưa thấy Hệ thống F và nhận được một số thực hành viết các loại đa hình, bạn sẽ thấy forallkhó hiểu. Kinh nghiệm với Haskell hoặc ML là không đủ, vì thông thường các ngôn ngữ này bỏ qua các forallloại đa hình. (Trong suy nghĩ của tôi, đây là một lỗi thiết kế ngôn ngữ.)

  • Trong Haskell nói riêng, forallđược sử dụng theo những cách mà tôi thấy khó hiểu. (Tôi không phải là một nhà lý thuyết loại, nhưng công việc của tôi mang lại cho tôi tiếp xúc với rất nhiều lý thuyết loại, và tôi khá thoải mái với nó.) Đối với tôi, nguồn gốc của sự nhầm lẫn là forallđược sử dụng để mã hóa một loại Bản thân tôi muốn viết với exists. Nó được chứng minh bằng một chút phức tạp của kiểu đẳng cấu liên quan đến định lượng và mũi tên, và mỗi khi tôi muốn hiểu nó, tôi phải tự tìm kiếm mọi thứ và tự mình tìm ra sự đẳng cấu.

    Nếu bạn không cảm thấy thoải mái với ý tưởng về kiểu đẳng cấu, hoặc nếu bạn không có bất kỳ suy nghĩ thực hành nào về kiểu đồng phân kiểu, việc sử dụng forallnày sẽ cản trở bạn.

  • Trong khi khái niệm chung forallluôn luôn giống nhau (ràng buộc để giới thiệu một biến loại), các chi tiết sử dụng khác nhau có thể thay đổi đáng kể. Tiếng Anh không chính thức không phải là một công cụ rất tốt để giải thích các biến thể. Để thực sự hiểu những gì đang xảy ra, bạn cần một số toán học. Trong trường hợp này, toán học có liên quan có thể được tìm thấy trong các loại văn bản giới thiệu và Ngôn ngữ lập trình của Benjamin Pierce , đây là một cuốn sách rất hay.

Đối với các ví dụ cụ thể của bạn,

  • runST nên làm cho đầu của bạn bị tổn thương. Các loại cấp cao hơn (nằm ở bên trái của một mũi tên) hiếm khi được tìm thấy trong tự nhiên. Tôi khuyến khích bạn đọc bài viết giới thiệu runST: "Chủ đề trạng thái chức năng lười biếng" . Đây là một bài báo thực sự tốt, và nó sẽ cung cấp cho bạn một trực giác tốt hơn nhiều cho loại runSTnói riêng và cho các loại cấp cao hơn nói chung. Lời giải thích mất vài trang, nó được thực hiện rất tốt, và tôi sẽ không cố gắng cô đọng nó ở đây.

  • Xem xét

    foo :: (forall a. a -> a) -> (Char,Bool)
    bar :: forall a. ((a -> a) -> (Char, Bool))

    Nếu tôi gọi bar, tôi chỉ có thể chọn bất kỳ loại anào tôi thích và tôi có thể chuyển cho nó một chức năng từ loại này asang loại khác a. Ví dụ, tôi có thể truyền hàm (+1)hoặc hàm reverse. Bạn có thể nghĩ về forallcâu nói "Tôi có thể chọn loại ngay bây giờ". (Từ kỹ thuật để chọn loại được instantiating .)

    Các hạn chế trong việc gọi foolà nghiêm ngặt hơn nhiều: đối số foo phải là một hàm đa hình. Với kiểu đó, các hàm duy nhất tôi có thể chuyển đến fooidhoặc một hàm luôn phân kỳ hoặc lỗi, như thế nào undefined. Lý do là với foo, các forallnằm bên trái của mũi tên, như vậy là người gọi của footôi không nhận được để chọn những gì alà-chứ không phải đó là thực hiện của foomà được chọn những gì alà. Vì forallnằm ở bên trái của mũi tên, thay vì phía trên mũi tên như trong bar, việc khởi tạo diễn ra trong phần thân của hàm chứ không phải tại vị trí cuộc gọi.

Tóm tắt: Một lời giải thích đầy đủ về foralltừ khóa yêu cầu toán học và chỉ có thể được hiểu bởi một người đã học toán. Ngay cả những lời giải thích một phần cũng khó hiểu nếu không có toán học. Nhưng có lẽ giải thích một phần, phi toán học của tôi giúp một chút. Hãy đọc Launchbury và Peyton Jones trên runST!


Phụ lục: Jargon "phía trên", "bên dưới", "bên trái". Những không có gì để làm với các văn bản cách loại được viết và tất cả mọi thứ để làm với cây trừu tượng-cú pháp. Trong cú pháp trừu tượng, a foralllấy tên của một biến loại, và sau đó có một kiểu đầy đủ "bên dưới" forall. Một mũi tên có hai loại (loại đối số và loại kết quả) và tạo thành một loại mới (loại chức năng). Kiểu đối số là "bên trái" mũi tên; nó là con trái của mũi tên trong cây cú pháp trừu tượng.

Ví dụ:

  • Trong forall a . [a] -> [a], forall là trên mũi tên; những gì bên trái của mũi tên là [a].

  • Trong

    forall n f e x . (forall e x . n e x -> f -> Fact x f) 
                  -> Block n e x -> f -> Fact x f

    loại trong ngoặc đơn sẽ được gọi là "một mũi tên ở bên trái của một mũi tên". (Tôi đang sử dụng các loại như thế này trong trình tối ưu hóa tôi đang làm việc.)


Trên thực tế tôi đã nhận được ở trên / dưới / bên trái của mà không phải suy nghĩ về nó. Tôi là một người đần độn, vâng, nhưng một người đần độn đã phải vật lộn với những thứ đó trước đây. (Viết trình biên dịch ASN.1 trong số những người khác.;) Tuy nhiên, cảm ơn vì phần phụ lục.
CHỈ CẦN HOẠT ĐỘNG CỦA TÔI NGÀY

12
@ CHỈ cảm ơn nhưng tôi đang viết cho hậu thế. Tôi đã gặp nhiều hơn một lập trình viên nghĩ rằng forall a . [a] -> [a], phần trước nằm ở bên trái của mũi tên.
Norman Ramsey

OK, đi qua câu trả lời của bạn một cách chi tiết, bây giờ, tôi phải cảm ơn bạn, Norman, từ tận đáy lòng của tôi. Rất nhiều thứ đã rơi vào vị trí với một tiếng click lớn và những thứ mà tôi vẫn không hiểu tôi ít nhất nhận ra rằng tôi không có ý hiểu nó và sẽ chỉ vượt qua foralltrong những trường hợp đó, một cách hiệu quả, dòng tiếng ồn. Tôi sẽ xem qua tờ giấy mà bạn đã liên kết (cảm ơn vì liên kết này!) Và xem nó có trong lĩnh vực hiểu biết của tôi không. Thanh danh.
CHỈ CẦN HOẠT ĐỘNG CỦA TÔI đúng

10
Tôi đọc trái và tôi nhìn, theo nghĩa đen, trái. Vì vậy, nó rất không rõ ràng với tôi cho đến khi bạn nói "cây phân tích".
Paul Nathan

Nhờ vào con trỏ đến cuốn sách của Pierce. Nó có một lời giải thích rất rõ ràng về Hệ thống F. Nó giải thích tại sao existskhông bao giờ được thực hiện. (Đây không phải là một phần của Hệ thống F!) Trong Haskell, một phần của Hệ thống F được ẩn giấu, nhưng foralllà một trong những điều không thể quét hoàn toàn dưới tấm thảm. Cứ như thể họ bắt đầu với Hindley-Milner, thứ sẽ cho phép forallđược ẩn giấu, và sau đó chọn một hệ thống loại mạnh hơn, gây nhầm lẫn cho những người trong chúng ta đã nghiên cứu 'forall' và 'tồn tại' của FOL và dừng lại ở đó.
T_S_

50

Câu trả lời ban đầu của tôi:

Bất cứ ai cũng có thể giải thích hoàn toàn từ khóa forall bằng tiếng Anh rõ ràng, đơn giản

Như Norman chỉ ra, rất khó để đưa ra một lời giải thích rõ ràng, rõ ràng bằng tiếng Anh về một thuật ngữ kỹ thuật từ lý thuyết loại. Tất cả chúng ta đều đang cố gắng.

Thực sự chỉ có một điều cần nhớ về 'forall': nó liên kết các loại với một phạm vi nào đó . Một khi bạn hiểu điều đó, mọi thứ khá dễ dàng. Nó tương đương với 'lambda' (hoặc một dạng 'cho phép') ở cấp độ loại - Norman Ramsey sử dụng khái niệm "trái" / "ở trên" để truyền đạt khái niệm phạm vi tương tự này trong câu trả lời xuất sắc của mình .

Hầu hết việc sử dụng 'forall' rất đơn giản và bạn có thể tìm thấy chúng được giới thiệu trong Hướng dẫn sử dụng GHC, S7.8 ., Đặc biệt là S7.8.5 tuyệt vời trên các hình thức 'forall' lồng nhau.

Trong Haskell, chúng ta thường loại bỏ chất kết dính cho các loại, khi loại được phổ biến toàn bộ, như vậy:

length :: forall a. [a] -> Int

tương đương với:

length :: [a] -> Int

Đó là nó.

Vì bây giờ bạn có thể liên kết các biến loại với một số phạm vi, bạn có thể có phạm vi khác với cấp cao nhất (" định lượng toàn cầu "), như ví dụ đầu tiên của bạn, trong đó biến loại chỉ hiển thị trong cấu trúc dữ liệu. Điều này cho phép các loại ẩn (" loại tồn tại "). Hoặc chúng ta có thể có các lồng liên kết tùy ý ("loại N xếp hạng").

Để hiểu sâu về các hệ thống loại, bạn sẽ cần phải học một số biệt ngữ. Đó là bản chất của khoa học máy tính. Tuy nhiên, sử dụng đơn giản, như trên, có thể được nắm bắt bằng trực giác, thông qua sự tương tự với 'cho phép' ở mức giá trị. Một giới thiệu tuyệt vời là Launchbury và Peyton Jones .


5
về mặt kỹ thuật, length :: forall a. [a] -> Intkhông tương đương với length :: [a] -> Intkhi ScopedTypeVariablesđược kích hoạt. Khi forall a.ở đó, nó ảnh hưởng đến length's wherekhoản (nếu nó có một) và thay đổi ý nghĩa của các biến kiểu tên atrong đó.
yairchu

2
Thật. ScopedTypeVariabled làm phức tạp câu chuyện một chút.
Don Stewart

3
@DonStewart, "nó có thể liên kết các loại với một phạm vi nào đó" tốt hơn là "nó liên kết các biến loại với một phạm vi nào đó" trong lời giải thích của bạn?
Romasy

31

Chúng dày đặc với các giả định mà tôi đã đọc mới nhất trong bất kỳ nhánh nào của toán học rời rạc, lý thuyết thể loại hoặc đại số trừu tượng là phổ biến trong tuần này. (Nếu tôi không bao giờ đọc các từ "tham khảo bài viết bất cứ điều gì để biết chi tiết thực hiện" một lần nữa, nó sẽ quá sớm.)

Er, còn logic thứ nhất đơn giản thì sao? foralllà khá rõ ràng liên quan đến định lượng phổ quát , và trong bối cảnh đó, thuật ngữ hiện sinh cũng có ý nghĩa hơn, mặc dù nó sẽ ít khó xử hơn nếu có một existstừ khóa. Việc định lượng có hiệu quả phổ biến hay tồn tại hay không phụ thuộc vào vị trí của bộ định lượng liên quan đến việc các biến được sử dụng ở phía nào của mũi tên hàm và tất cả đều hơi khó hiểu.

Vì vậy, nếu điều đó không có ích, hoặc nếu bạn không thích logic biểu tượng, từ góc độ lập trình chức năng nhiều hơn, bạn có thể nghĩ các biến kiểu chỉ là tham số kiểu (ẩn) cho hàm. Các hàm lấy tham số kiểu theo nghĩa này được viết theo cách truyền thống sử dụng lambda vốn vì bất kỳ lý do gì, mà tôi sẽ viết ở đây là /\.

Vì vậy, hãy xem xét idchức năng:

id :: forall a. a -> a
id x = x

Chúng ta có thể viết lại nó dưới dạng lambdas, di chuyển "tham số kiểu" ra khỏi chữ ký loại và thêm chú thích kiểu nội tuyến:

id = /\a -> (\x -> x) :: a -> a

Đây là điều tương tự được thực hiện để const:

const = /\a b -> (\x y -> x) :: a -> b -> a

Vì vậy, barchức năng của bạn có thể là một cái gì đó như thế này:

bar = /\a -> (\f -> ('t', True)) :: (a -> a) -> (Char, Bool)

Lưu ý rằng loại hàm được đưa ra barlàm đối số phụ thuộc vào bartham số loại của. Hãy xem xét nếu bạn có một cái gì đó như thế này thay vào đó:

bar2 = /\a -> (\f -> (f 't', True)) :: (a -> a) -> (Char, Bool)

Ở đây bar2đang áp dụng hàm cho một cái gì đó thuộc loại Char, do đó việc đưa ra bar2bất kỳ tham số loại nào khác ngoài Charsẽ gây ra lỗi loại.

Mặt khác, đây là những gì foocó thể trông như:

foo = (\f -> (f Char 't', f Bool True))

Không giống như bar, foothực sự không có bất kỳ tham số loại nào! Phải mất một chức năng mà bản thân phải mất một tham số kiểu, sau đó áp dụng chức năng đó để hai khác nhau các loại.

Vì vậy, khi bạn nhìn thấy một forallchữ ký loại, chỉ cần nghĩ về nó như một biểu thức lambda cho chữ ký loại . Giống như lambdas thông thường, phạm vi forallmở rộng càng xa về phía bên phải càng tốt, cho đến bao quanh dấu ngoặc đơn và giống như các biến bị ràng buộc trong lambda thông thường, các biến loại bị ràng buộc bởi một forallphạm vi chỉ trong phạm vi biểu thức được định lượng.


Post scriptum : Có lẽ bạn có thể tự hỏi - bây giờ chúng ta đang nghĩ về các hàm lấy tham số loại, tại sao chúng ta không thể làm điều gì đó thú vị hơn với các tham số đó hơn là đặt chúng vào chữ ký loại? Câu trả lời là chúng ta có thể!

Hàm đặt các biến loại cùng với nhãn và trả về một kiểu mới là hàm tạo kiểu , bạn có thể viết một cái gì đó như thế này:

Either = /\a b -> ...

Nhưng chúng ta cần ký hiệu hoàn toàn mới, bởi vì cách viết như vậy Either a b, giống như , đã gợi ý "áp dụng hàm Eithercho các tham số này".

Mặt khác, một hàm sắp xếp "khớp mẫu" trên các tham số loại của nó, trả về các giá trị khác nhau cho các loại khác nhau, là một phương thức của một lớp loại . Một mở rộng nhỏ cho /\cú pháp của tôi ở trên gợi ý một cái gì đó như thế này:

fmap = /\ f a b -> case f of
    Maybe -> (\g x -> case x of
        Just y -> Just b g y
        Nothing -> Nothing b) :: (a -> b) -> Maybe a -> Maybe b
    [] -> (\g x -> case x of
        (y:ys) -> g y : fmap [] a b g ys 
        []     -> [] b) :: (a -> b) -> [a] -> [b]

Cá nhân, tôi nghĩ rằng tôi thích cú pháp thực tế của Haskell ...

Một hàm "mẫu khớp" với các tham số kiểu của nó và trả về một kiểu tùy ý, kiểu hiện có là một kiểu họ hoặc phụ thuộc hàm - trong trường hợp trước, nó thậm chí trông rất giống định nghĩa hàm.


1
Một điều thú vị ở đây. Điều này cho tôi một góc tấn công khác vào vấn đề có thể chứng minh có kết quả trong dài hạn. Cảm ơn.
CHỈ CẦN HOẠT ĐỘNG CHÍNH XÁC CỦA TÔI

@KennyTM: Hoặc λcho vấn đề đó, nhưng phần mở rộng cú pháp unicode của GHC không hỗ trợ điều đó bởi vì λ là một chữ cái , một thực tế đáng tiếc cũng có thể áp dụng cho các khái niệm trừu tượng lớn-lambda của tôi. Do đó /\ bằng cách tương tự với \ . Tôi cho rằng tôi có thể vừa mới sử dụng nhưng tôi đã cố tránh tính toán vị ngữ ...
CA McCann

29

Dưới đây là một lời giải thích nhanh chóng và bẩn thỉu theo nghĩa đơn giản mà bạn có thể đã quen thuộc.

Các foralltừ khóa được thực sự chỉ được sử dụng theo cách này trong Haskell. Nó luôn có nghĩa tương tự khi bạn nhìn thấy nó.

Định lượng toàn cầu

Một loại định lượng phổ quát là một loại hình thức forall a. f a. Một giá trị của loại đó có thể được coi là một hàm lấy một loại a làm đối số của nó và trả về một giá trị của loại f a. Ngoại trừ trong Haskell, các đối số kiểu này được hệ thống kiểu truyền vào hoàn toàn. "Hàm" này phải cung cấp cho bạn cùng một giá trị cho dù nó nhận loại nào, vì vậy giá trị này là đa hình .

Ví dụ, hãy xem xét các loại forall a. [a]. Một giá trị của loại đó lấy một loại khác avà cung cấp cho bạn một danh sách các phần tử cùng loại đó a. Tất nhiên chỉ có một triển khai có thể. Nó sẽ phải cung cấp cho bạn danh sách trống vì acó thể hoàn toàn là bất kỳ loại nào. Danh sách trống là giá trị danh sách duy nhất là đa hình trong loại phần tử của nó (vì nó không có phần tử).

Hoặc các loại forall a. a -> a. Người gọi của một chức năng như vậy cung cấp cả một loại avà một giá trị của loại a. Việc thực hiện sau đó phải trả về một giá trị cùng loại a. Chỉ có một thực hiện có thể một lần nữa. Nó sẽ phải trả lại cùng giá trị mà nó đã được đưa ra.

Định lượng hiện sinh

Một loại được định lượng tồn tại sẽ có dạng exists a. f a, nếu Haskell hỗ trợ ký hiệu đó. Một giá trị của loại đó có thể được coi là một cặp (hoặc "sản phẩm") bao gồm một loại avà một giá trị của loại f a.

Ví dụ: nếu bạn có một giá trị của loại exists a. [a], bạn có một danh sách các yếu tố của một số loại. Nó có thể là bất kỳ loại nào, nhưng ngay cả khi bạn không biết đó là những gì bạn có thể làm với danh sách đó. Bạn có thể đảo ngược nó hoặc bạn có thể đếm số lượng phần tử hoặc thực hiện bất kỳ thao tác danh sách nào khác không phụ thuộc vào loại phần tử.

OK, vậy hãy đợi một chút. Tại sao Haskell sử dụng forallđể biểu thị một loại "tồn tại" như sau?

data ShowBox = forall s. Show s => SB s

Nó có thể gây nhầm lẫn, nhưng nó thực sự mô tả loại công cụ xây dựng dữ liệu SB :

SB :: forall s. Show s => s -> ShowBox

Sau khi được xây dựng, bạn có thể nghĩ về một giá trị của loại ShowBoxbao gồm hai điều. Đó là một loại scùng với một giá trị của loại s. Nói cách khác, đó là một giá trị của một loại định lượng tồn tại. ShowBoxthực sự có thể được viết là exists s. Show s => s, nếu Haskell ủng hộ ký hiệu đó.

runST và những người bạn

Cho rằng, làm thế nào là khác nhau?

foo :: (forall a. a -> a) -> (Char,Bool)
bar :: forall a. ((a -> a) -> (Char, Bool))

Trước tiên hãy lấy bar. Nó nhận một loại avà một chức năng của loại a -> a, và tạo ra một giá trị của loại (Char, Bool). Chúng ta có thể chọn Intlàm avà cung cấp cho nó một chức năng của loại Int -> Intví dụ. Nhưng foolà khác nhau. Nó đòi hỏi việc thực hiện foocó thể vượt qua bất kỳ loại nào nó muốn cho chức năng chúng ta cung cấp cho nó. Vì vậy, chức năng duy nhất chúng ta có thể cung cấp cho nó là hợp lý id.

Bây giờ chúng ta có thể giải quyết ý nghĩa của loại runST:

runST :: forall a. (forall s. ST s a) -> a

Vì vậy, runSTphải có khả năng tạo ra một giá trị của loại a, bất kể chúng ta đưa ra loại nào a. Để làm như vậy, nó sử dụng một đối số kiểu forall s. ST s amà chắc chắn phải tạo ra a. Hơn nữa, nó phải có khả năng tạo ra một giá trị của loại cho adù việc thực hiện runSTquyết định đưa ra là loại nào s.

Được, vậy thì sao? Lợi ích là điều này đặt ra một ràng buộc đối với người gọi runSTtrong đó loại akhông thể liên quan đến loại này s. Bạn không thể vượt qua nó một giá trị của loại ST s [s], ví dụ. Điều đó có nghĩa là trong thực tế là việc thực hiện runSTlà miễn phí để thực hiện đột biến với giá trị của loại s. Loại đảm bảo rằng đột biến này là cục bộ để thực hiện runST.

Kiểu của runSTlà một ví dụ về loại đa hình bậc 2 vì loại đối số của nó chứa bộ forallđịnh lượng. Loại fooở trên cũng thuộc hạng 2. Một loại đa hình thông thường, giống như barloại 1, nhưng nó trở thành hạng 2 nếu các loại đối số được yêu cầu là đa hình, với bộ forallđịnh lượng riêng . Và nếu một hàm lấy các đối số hạng 2 thì kiểu của nó là hạng 3, v.v. Nói chung, một loại có các đối số đa hình về thứ hạng ncó thứ hạng n + 1.


11

Bất cứ ai cũng có thể giải thích hoàn toàn từ khóa forall bằng tiếng Anh rõ ràng, đơn giản (hoặc, nếu nó tồn tại ở đâu đó, chỉ ra một lời giải thích rõ ràng mà tôi đã bỏ qua) mà không cho rằng tôi là một nhà toán học chìm trong thuật ngữ?

Tôi sẽ cố gắng giải thích ý nghĩa và có thể là ứng dụng foralltrong bối cảnh của Haskell và các hệ thống loại của nó.

Nhưng trước khi bạn hiểu rằng tôi muốn hướng bạn đến một cuộc nói chuyện rất dễ tiếp cận và hay của Runar Bjarnason với tiêu đề " Những ràng buộc giải phóng, ràng buộc tự do ". Cuộc nói chuyện có đầy đủ các ví dụ từ các trường hợp sử dụng trong thế giới thực cũng như các ví dụ trong Scala để hỗ trợ cho tuyên bố này, mặc dù nó không đề cập đến forall. Tôi sẽ cố gắng giải thích forallquan điểm dưới đây.

                CONSTRAINTS LIBERATE, LIBERTIES CONSTRAIN

Nó rất quan trọng để tiêu hóa và tin vào tuyên bố này để tiến hành giải thích sau đây, vì vậy tôi khuyên bạn nên xem cuộc nói chuyện (ít nhất là một phần của nó).

Bây giờ là một ví dụ rất phổ biến, cho thấy tính biểu cảm của hệ thống loại Haskell là chữ ký loại này:

foo :: a -> a

Người ta nói rằng với chữ ký loại này, chỉ có một chức năng có thể đáp ứng loại này và đó là identitychức năng hoặc những gì phổ biến hơn được biết đến id.

Trong giai đoạn đầu tôi học Haskell, tôi luôn tự hỏi các chức năng dưới đây:

foo 5 = 6

foo True = False

cả hai đều thỏa mãn chữ ký loại trên, vậy tại sao mọi người Haskell tuyên bố rằng nó là idmột mình thỏa mãn chữ ký loại?

Đó là bởi vì có một ẩn forallẩn trong chữ ký loại. Loại thực tế là:

id :: forall a. a -> a

Vì vậy, bây giờ chúng ta hãy quay trở lại với tuyên bố: Những ràng buộc giải phóng, tự do ràng buộc

Dịch nó sang hệ thống loại, tuyên bố này trở thành:

Một ràng buộc ở cấp độ loại, trở thành tự do ở cấp độ hạn

Một quyền tự do ở cấp độ loại, trở thành một ràng buộc ở cấp độ hạn


Chúng ta hãy cố gắng chứng minh tuyên bố đầu tiên:

Một ràng buộc ở cấp độ loại ..

Vì vậy, đặt một ràng buộc trên chữ ký loại của chúng tôi

foo :: (Num a) => a -> a

trở thành một tự do ở cấp độ thuật ngữ cho chúng ta sự tự do hoặc linh hoạt để viết tất cả những điều này

foo 5 = 6
foo 4 = 2
foo 7 = 9
...

Điều tương tự có thể được quan sát bằng cách ràng buộc avới bất kỳ kiểu chữ nào khác, v.v.

Vì vậy, bây giờ những gì chữ ký loại này: foo :: (Num a) => a -> adịch là:

a , st a -> a, a  Num

Điều này được gọi là định lượng hiện sinh, nghĩa là tồn tại một số trường hợp trong ađó một hàm khi được cung cấp một loại nào ađó trả về một cái gì đó cùng loại và tất cả các trường hợp đó đều thuộc về bộ Số.

Do đó, chúng ta có thể thấy việc thêm một ràng buộc ( anên thuộc về bộ Số), giải phóng cấp độ thuật ngữ để có nhiều triển khai có thể.


Bây giờ đến với tuyên bố thứ hai và một trong đó thực sự mang lời giải thích về forall:

Một quyền tự do ở cấp độ loại, trở thành một ràng buộc ở cấp độ hạn

Vì vậy, bây giờ chúng ta hãy giải phóng chức năng ở cấp độ loại:

foo :: forall a. a -> a

Bây giờ điều này dịch sang:

a , a -> a

có nghĩa là việc thực hiện chữ ký loại này phải a -> aphù hợp với mọi hoàn cảnh.

Vì vậy, bây giờ điều này bắt đầu ràng buộc chúng tôi ở cấp độ hạn. Chúng tôi không còn có thể viết

foo 5 = 7

bởi vì việc thực hiện này sẽ không thỏa mãn nếu chúng ta đặt anhư một Bool. acó thể là một Charhoặc một [Char]kiểu dữ liệu tùy chỉnh. Trong mọi trường hợp, nó sẽ trả về một cái gì đó thuộc loại tương tự. Sự tự do này ở cấp độ loại là cái được gọi là Định lượng phổ quát và chức năng duy nhất có thể đáp ứng điều này là

foo a = a

thường được gọi là identityhàm


Do đó foralllà một libertyở cấp loại, mà mục đích thực tế là constrainmức độ hạn để thực hiện một cụ thể.


9

Lý do tại sao có những cách sử dụng khác nhau của từ khóa này là vì nó thực sự được sử dụng trong ít nhất hai phần mở rộng hệ thống loại khác nhau: loại có thứ hạng cao hơn và tồn tại.

Có lẽ tốt nhất là chỉ nên đọc và hiểu hai điều đó một cách riêng biệt, thay vì cố gắng giải thích lý do tại sao 'forall' là một cú pháp thích hợp trong cả hai cùng một lúc.


3

Làm thế nào là tồn tại tồn tại?

Với Định lượng Hiện sinh, foralls trong các datađịnh nghĩa có nghĩa là, giá trị chứa thể thuộc bất kỳ loại phù hợp nào , không phảitất cả các loại phù hợp. - câu trả lời của yachiru

Một lời giải thích tại sao foralltrong các datađịnh nghĩa là đẳng cấu với (exists a. a)(pseudo-Haskell) có thể được tìm thấy trong "Haskell / Các loại định lượng hiện tại" của wikibooks .

Sau đây là tóm tắt nguyên văn ngắn gọn:

data T = forall a. MkT a -- an existential datatype
MkT :: forall a. a -> T -- the type of the existential constructor

Khi khớp mẫu / giải cấu trúc MkT x, loại là xgì?

foo (MkT x) = ... -- -- what is the type of x?

xcó thể là bất kỳ loại nào (như đã nêu trong forall), và vì vậy loại đó là:

x :: exists a. a -- (pseudo-Haskell)

Do đó, sau đây là đẳng cấu:

data T = forall a. MkT a -- an existential datatype
data T = MkT (exists a. a) -- (pseudo-Haskell)

forall có nghĩa là forall

Giải thích đơn giản của tôi về tất cả những điều này, là " forallthực sự có nghĩa là" cho tất cả "". Một khác biệt quan trọng để làm cho là tác động của foralltrên nét so với chức năng ứng dụng .

A forallcó nghĩa là định nghĩa của giá trị hoặc hàm phải là đa hình.

Nếu thứ được định nghĩa là một giá trị đa hình , thì điều đó có nghĩa là giá trị đó phải hợp lệ cho tất cả các giá trị phù hợp a, điều này khá hạn chế.

Nếu vật được định nghĩa là hàm đa hình , thì điều đó có nghĩa là hàm đó phải hợp lệ cho tất cả phù hợp a, điều đó không hạn chế vì chỉ vì hàm đa hình không có nghĩa là tham số được áp dụng phải là đa hình. Đó là, nếu chức năng hợp lệ cho tất cả a, thì ngược lại, bất kỳ sự phù hợp nàoa cũng có thể được áp dụng cho chức năng. Tuy nhiên, loại tham số chỉ có thể được chọn một lần trong định nghĩa hàm.

Nếu a forallnằm trong loại tham số của hàm (nghĩa là a Rank2Type) thì có nghĩa là tham số được áp dụng phải thực sự đa hình, để phù hợp với ý tưởng về định nghĩaforall phương tiện là đa hình. Trong trường hợp này, loại tham số có thể được chọn nhiều lần trong định nghĩa hàm ( "và được chọn bằng cách thực hiện hàm", như được chỉ ra bởi Norman )

Do đó, lý do tại sao các datađịnh nghĩa hiện sinh cho phép bất kỳ a là vì hàm tạo dữ liệu là một hàm đa hình :

MkT :: forall a. a -> T

loại MkT :: a -> *

Có nghĩa là bất kỳ acó thể được áp dụng cho chức năng. Trái ngược với, một giá trị đa hình :

valueT :: forall a. [a]

loại giá trịT :: a

Điều đó có nghĩa là định nghĩa của valueT phải là đa hình. Trong trường hợp này, valueTcó thể được định nghĩa là danh sách trống []của tất cả các loại.

[] :: [t]

Sự khác biệt

Mặc dù ý nghĩa của foralllà nhất quán trong ExistentialQuantificationRankNType, các tồn tại có một sự khác biệt vì hàm datatạo có thể được sử dụng trong khớp mẫu. Như tài liệu trong hướng dẫn sử dụng ghc :

Khi khớp mẫu, mỗi khớp mẫu sẽ đưa ra một kiểu mới, riêng biệt cho từng biến kiểu tồn tại. Các loại này không thể thống nhất với bất kỳ loại nào khác và cũng không thể thoát khỏi phạm vi khớp mẫu.

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.