Các loại Haskell làm nản lòng một hàm 'trung bình' đơn giản


76

Tôi đang chơi với Haskell mới bắt đầu và tôi muốn viết một hàm trung bình. Nó dường như là điều đơn giản nhất trên thế giới, phải không?

Sai lầm.

Có vẻ như hệ thống kiểu của Haskell cấm trung bình làm việc trên một kiểu số chung - tôi có thể làm cho nó hoạt động trên danh sách Tích phân hoặc danh sách Phân số, nhưng không phải cả hai.

Tôi muốn:

average :: (Num a, Fractional b) => [a] -> b
average xs = ...

Nhưng tôi chỉ có thể nhận được:

averageInt :: (Integral a, Fractional b) => [a] -> b
averageInt xs = fromIntegral (sum xs) / fromIntegral (length xs)

hoặc là

averageFrac :: (Fractional a) => [a] -> a
averageFrac xs = sum xs / fromIntegral (length xs)

và cái thứ hai có vẻ hoạt động. Cho đến khi tôi cố gắng chuyển một biến.

*Main> averageFrac [1,2,3]
2.0
*Main> let x = [1,2,3]
*Main> :t x
x :: [Integer]
*Main> averageFrac x

<interactive>:1:0:
    No instance for (Fractional Integer)
      arising from a use of `averageFrac ' at <interactive>:1:0-8
    Possible fix: add an instance declaration for (Fractional Integer)
    In the expression: average x
    In the definition of `it': it = averageFrac x

Rõ ràng, Haskell thực sự rất kén chọn các loại của nó. Điều đó có lý. Nhưng không phải khi cả hai đều có thể [Num]

Tôi có thiếu một ứng dụng rõ ràng của RealFrac không?

Có cách nào để ép Tích phân vào Phân số không bị nghẹt thở khi nhận được đầu vào Phân số không?

Có cách nào để sử dụng Eithereithertạo một số loại hàm trung bình đa hình có thể hoạt động trên bất kỳ loại mảng số nào không?

Hệ thống loại của Haskell có cấm hoàn toàn chức năng này tồn tại không?

Học Haskell cũng giống như học Giải tích. Nó thực sự phức tạp và dựa trên hàng núi lý thuyết, và đôi khi vấn đề phức tạp đến mức tôi thậm chí không biết đủ để diễn đạt câu hỏi một cách chính xác, vì vậy bất kỳ cái nhìn sâu sắc nào cũng sẽ được chấp nhận một cách nồng nhiệt.

(Ngoài ra, chú thích cuối trang: điều này dựa trên vấn đề bài tập về nhà. Mọi người đều đồng ý rằng trung bình Frac, ở trên, nhận được đầy đủ điểm, nhưng tôi nghi ngờ rằng có một cách để làm cho nó hoạt động trên cả mảng Tích phân và Phân số)


Câu trả lời:


106

Vì vậy, về cơ bản, bạn bị hạn chế bởi loại (/):

(/) :: (Fractional a) => a -> a -> a

BTW, bạn cũng muốn Data.List.genericLength

genericLength :: (Num i) => [b] -> i

Vì vậy, làm thế nào về việc loại bỏ fromIntegral cho một cái gì đó tổng quát hơn:

import Data.List

average xs = realToFrac (sum xs) / genericLength xs

mà chỉ có một ràng buộc Thực (Int, Integer, Float, Double) ...

average :: (Real a, Fractional b) => [a] -> b

Vì vậy, điều đó sẽ đưa bất kỳ Real nào vào bất kỳ Phân số nào.

Và lưu ý tất cả các áp phích bị bắt bởi các ký tự số đa hình trong Haskell. 1 không phải là một số nguyên, nó là một số bất kỳ.

Lớp Real chỉ cung cấp một phương thức: khả năng biến một giá trị trong lớp Num thành một giá trị hợp lý. Đó là chính xác những gì chúng ta cần ở đây.

Và như vậy,

Prelude> average ([1 .. 10] :: [Double])
5.5
Prelude> average ([1 .. 10] :: [Int])
5.5
Prelude> average ([1 .. 10] :: [Float])
5.5
Prelude> average ([1 .. 10] :: [Data.Word.Word8])
5.5

nhưng điều gì sẽ xảy ra nếu tôi muốn gọi trung bình trên một danh sách, chẳng hạn như Đôi? Có một hàm giống như numToFrac nhận Real hoặc Fractional và trả về một Fractional không? Chúng ta có thể viết một?
jakebman

5
Bạn có thể cung cấp cho nó một danh sách các Đôi, vì Đôi đang ở Thực. "trung bình ([1 .. 10] :: [Double])". Lớp Real bổ sung chính xác khả năng xây dựng một giá trị hợp lý từ những thứ trong Num. Đó chính xác là những gì bạn cần.
Don Stewart

Bạn đúng! Cảm ơn vì đã làm rõ điều đó! Có bất kỳ loại nào trong Num mà realToFrac không hoạt động không? Tôi không thể hiểu tại sao nó không phải là numToFrac.
jakebman

4
Không thể viết numToFrac, vì Num không cung cấp bất kỳ hàm chuyển đổi nào. Thực là thứ gần nhất mà chúng ta có (Số loại có thể được chuyển đổi thành Hợp lý), hoặc Tích phân (Số loại có thể được chuyển đổi thành Số nguyên không bị ràng buộc).
Don Stewart

Cảm ơn! Tôi đã bị đánh lừa bởi biểu đồ trên trang cuối cùng của cs.ut.ee/~varmo/MFP2004/PreludeTour.pdf cho thấy Floating KHÔNG kế thừa các thuộc tính từ Real và sau đó tôi cho rằng chúng sẽ không có chung loại nào. Ít nhất tôi có thể học hỏi từ những sai lầm của mình.
jakebman

25

Câu hỏi đã được trả lời rất tốt bởi Dons, tôi nghĩ rằng tôi có thể thêm một cái gì đó.

Khi tính giá trị trung bình theo cách này:

average xs = realToFrac (sum xs) / genericLength xs

Những gì mã của bạn sẽ làm là duyệt qua danh sách hai lần, một lần để tính tổng các phần tử của nó và một lần để tính độ dài của nó. Theo như tôi biết, GHC vẫn chưa thể tối ưu hóa điều này và tính cả tổng và độ dài trong một lần chuyển.

Ngay cả khi người mới bắt đầu phải nghĩ về nó và về các giải pháp khả thi, chẳng hạn như hàm trung bình có thể được viết bằng cách sử dụng hàm gấp để tính cả tổng và độ dài; trên ghci:

:set -XBangPatterns

import Data.List

let avg l=let (t,n) = foldl' (\(!b,!c) a -> (a+b,c+1)) (0,0) l in realToFrac(t)/realToFrac(n)

avg ([1,2,3,4]::[Int])
2.5
avg ([1,2,3,4]::[Double])
2.5

Chức năng trông không thanh lịch, nhưng hiệu suất tốt hơn.

Thông tin thêm trên blog của Dons:

http://donsbot.wordpress.com/2008/06/04/haskell-as-fast-as-c-working-at-a-high-altitude-for-low-level-performance/


4
+1 cho việc đề xuất màn hình đầu tiên để tăng hiệu suất tốt hơn nhưng tôi chỉ khuyên bạn nên thử hiệu suất khi bạn hiểu đầy đủ về Haskell và tính đa hình của số nguyên là trò chơi của trẻ đối với bạn.
Robert Massaioli

11
+1 cho nhận xét của Robert Massaioli, vì mức trung bình này trên thực tế rất tệ ... gấp nếp là nghiêm ngặt trong bộ tích lũy, đối với Haskell có nghĩa là "dạng bình thường đầu yếu" về cơ bản, nghiêm ngặt đối với Trình tạo dữ liệu đầu tiên. Vì vậy, ví dụ ở đây, foldl 'sẽ đảm bảo rằng bộ tích lũy sẽ được đánh giá đủ để xác định đó là một cặp (phương thức tạo dữ liệu đầu tiên), nhưng nội dung sẽ không được đánh giá và do đó tích lũy các yếu tố. Sử dụng foldl' (\(!b,!c) a -> ...hoặc một loại cặp nghiêm ngặt data P a = P !a !alà chìa khóa để có được hiệu suất tốt trong trường hợp này.
Jedai

+1 cho nhận xét của Jedai, tôi đã chỉnh sửa bài đăng cho phù hợp.
David V.

9

Vì dons đã làm rất tốt trong việc trả lời câu hỏi của bạn, tôi sẽ làm việc để đặt câu hỏi cho câu hỏi của bạn ....

Ví dụ: trong câu hỏi của bạn, nơi đầu tiên bạn chạy điểm trung bình trên một danh sách nhất định, sẽ nhận được một câu trả lời tốt. Sau đó, bạn lấy những gì trông giống như cùng một danh sách , gán nó cho một biến, sau đó sử dụng hàm biến ... mà sau đó sẽ nổ tung.

Những gì bạn đã chạy vào đây là một thiết lập trong trình biên dịch, được gọi là DMR: các D readed M onomorphic R estriction. Khi bạn chuyển thẳng danh sách vào hàm, trình biên dịch không đưa ra giả định về loại số nào, nó chỉ suy ra loại nó có thể dựa trên cách sử dụng, và sau đó chọn một khi nó không thể thu hẹp trường nữa. Nó giống như đối lập trực tiếp với kiểu gõ vịt vậy.

Dù sao đi nữa, khi bạn gán danh sách cho một biến, DMR đã khởi động. Vì bạn đã đặt danh sách vào một biến, nhưng không đưa ra gợi ý về cách bạn muốn sử dụng nó, DMR đã khiến trình biên dịch chọn một kiểu, trong đó trường hợp, nó nhặt một trong những lần xuất hiện hình thức và dường như để phù hợp với: Integer. Vì hàm của bạn không thể sử dụng Số nguyên trong /hoạt động của nó (nó cần một kiểu trong Fractionallớp), nên nó khiến bạn rất phàn nàn: không có trường hợp nào Integertrong Fractionallớp. Có những tùy chọn bạn có thể đặt trong GHC để nó không buộc các giá trị của bạn vào một dạng duy nhất ("mono-morphic", hiểu không?) Cho đến khi cần, nhưng nó khiến bất kỳ thông báo lỗi nào hơi khó tìm ra.

Bây giờ, trên một ghi chú khác, bạn đã có câu trả lời cho câu trả lời của dons 'khiến tôi chú ý:

Tôi đã bị đánh lừa bởi biểu đồ trên trang cuối cùng của cs.ut.ee/~varmo/MFP2004/PreludeTour.pdf cho thấy Floating KHÔNG kế thừa các thuộc tính từ Real và sau đó tôi cho rằng chúng sẽ không có chung loại nào.

Haskell thực hiện các loại khác với những gì bạn đã quen. RealFloatinglà các lớp kiểu, hoạt động giống giao diện hơn là các lớp đối tượng. Họ cho bạn biết bạn có thể làm gì với một kiểu trong lớp đó, nhưng không có nghĩa là một số kiểu không thể làm những việc khác, ngoài việc có một giao diện nghĩa là một lớp (n kiểu OO) không thể có bất kỳ người khác.

Học Haskell giống như học Giải tích

Tôi muốn nói rằng học Haskell cũng giống như học tiếng Thụy Điển - có rất nhiều thứ nhỏ, đơn giản (chữ cái, số) trông và hoạt động giống nhau, nhưng cũng có những từ trông giống như chúng phải có nghĩa một điều, khi chúng thực sự có nghĩa. khác. Nhưng một khi bạn đã thông thạo nó, bạn bè thường xuyên của bạn sẽ ngạc nhiên về cách bạn có thể nói ra thứ kỳ quặc này khiến những người đẹp lộng lẫy làm những trò tuyệt vời. Thật kỳ lạ, có rất nhiều người tham gia vào Haskell ngay từ đầu, những người cũng biết tiếng Thụy Điển. Có lẽ phép ẩn dụ đó không chỉ là một phép ẩn dụ ...



-6

Phải, hệ thống loại của Haskell rất kén chọn. Vấn đề ở đây là loại fromIntegral:

Prelude> :t fromIntegral
fromIntegral :: (Integral a, Num b) => a -> b

fromIntegral sẽ chỉ chấp nhận Integral là một, không chấp nhận bất kỳ loại Num nào khác. (/), mặt khác chỉ chấp nhận phân số. Làm thế nào để bạn làm cho cả hai làm việc cùng nhau?

Chà, hàm sum là một khởi đầu tốt:

Prelude> :t sum
sum :: (Num a) => [a] -> a

Sum lấy một danh sách bất kỳ Num nào và trả về Num.

Vấn đề tiếp theo của bạn là độ dài của danh sách. Độ dài là một Int:

Prelude> :t length
length :: [a] -> Int

Bạn cũng cần chuyển đổi Int đó thành Num. Đó là những gì fromIntegral làm.

Vì vậy, bây giờ bạn đã có một hàm trả về Num và một hàm khác trả về Num. Bạn có thể tra cứu một số quy tắc về loại quảng cáo số , nhưng về cơ bản tại thời điểm này, bạn nên thực hiện:

Prelude> let average xs = (sum xs) / (fromIntegral (length xs))
Prelude> :t average
average :: (Fractional a) => [a] -> a

Hãy chạy thử:

Prelude> average [1,2,3,4,5]
3.0
Prelude> average [1.2,3.4,5.6,7.8,9.0]
5.4
Prelude> average [1.2,3,4.5,6,7.8,9]
5.25

11
Bạn cũng đã mắc phải cái bẫy tương tự như Michael. Quá tải số! 5 không phải là một giá trị tích phân. Nó là bất kỳ Num. Ở đây nó mặc định là giá trị phân số - bạn không thể chuyển vào Int hoặc Integer. - vì bạn sẽ nhận được Không có trường hợp nào cho (Fractional Int)
Don Stewart

Vâng, tệ của tôi ở đó. Tôi đã không chú ý đủ kỹ. Tôi đoán đây chính là lý do tại sao tôi chưa bao giờ làm được nhiều việc hơn là lập trình đồ chơi ở Haskell. Ngay cả với tư cách là một người yêu thích ngôn ngữ trói buộc và kỷ luật, tôi thấy Haskell là một bậc thầy hơi tàn bạo.
CHỈ LÀ Ý KIẾN chính xác của TÔI

2
Haskell không phải là B&D. Tiếp cận nó như B&D sẽ rất khó khăn. Nếu bạn muốn học Haskell, bạn sẽ cần phải nắm vững hệ thống loại. Thats tất cả để có nó. Sự nhầm lẫn của bạn sẽ biến mất khi bạn có cách sử dụng các loại cho bạn .
nomen
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.