Benjamin Pierce nói trong TAPL
Một hệ thống loại có thể được coi là tính toán một loại xấp xỉ tĩnh đối với các hành vi trong thời gian chạy của các điều khoản trong một chương trình.
Đó là lý do tại sao một ngôn ngữ được trang bị một hệ thống loại mạnh mẽ lại biểu cảm rõ ràng hơn là một ngôn ngữ được đánh máy kém. Bạn có thể nghĩ về các đơn nguyên theo cùng một cách.
Là điểm @Carl và sigfpe , bạn có thể trang bị một kiểu dữ liệu với tất cả các hoạt động bạn muốn mà không cần dùng đến các đơn nguyên, kiểu chữ hoặc bất kỳ nội dung trừu tượng nào khác. Tuy nhiên, các đơn vị cho phép bạn không chỉ viết mã có thể tái sử dụng mà còn để trừu tượng hóa tất cả các chi tiết dư thừa.
Ví dụ: giả sử chúng tôi muốn lọc một danh sách. Cách đơn giản nhất là sử dụng filter
hàm : filter (> 3) [1..10]
, bằng [4,5,6,7,8,9,10]
.
Một phiên bản phức tạp hơn một chút filter
, cũng chuyển một bộ tích lũy từ trái sang phải, là
swap (x, y) = (y, x)
(.*) = (.) . (.)
filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]
Để có được tất cả i
, như vậy i <= 10, sum [1..i] > 4, sum [1..i] < 25
, chúng ta có thể viết
filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]
mà bằng [3,4,5,6]
.
Hoặc chúng ta có thể xác định lại nub
hàm, loại bỏ các phần tử trùng lặp khỏi danh sách, theo filterAccum
:
nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []
nub' [1,2,4,5,4,3,1,8,9,4]
bằng [1,2,4,5,3,8,9]
. Một danh sách được thông qua như là một tích lũy ở đây. Mã này hoạt động, bởi vì nó có thể rời khỏi danh sách đơn nguyên, vì vậy toàn bộ tính toán vẫn ở dạng thuần túy ( thực tế notElem
không sử dụng >>=
, nhưng nó có thể). Tuy nhiên, không thể rời khỏi đơn vị IO một cách an toàn (nghĩa là bạn không thể thực hiện hành động IO và trả về giá trị thuần túy - giá trị sẽ luôn được gói trong đơn vị IO). Một ví dụ khác là mảng có thể thay đổi: sau khi bạn đã nhảy đơn vị ST, nơi một mảng có thể thay đổi trực tiếp, bạn không thể cập nhật mảng trong thời gian liên tục nữa. Vì vậy, chúng ta cần một bộ lọc đơn âm từ Control.Monad
mô-đun:
filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ [] = return []
filterM p (x:xs) = do
flg <- p x
ys <- filterM p xs
return (if flg then x:ys else ys)
filterM
thực hiện một hành động đơn nguyên cho tất cả các yếu tố từ một danh sách, mang lại các yếu tố mà hành động đơn nguyên đó trả về True
.
Một ví dụ lọc với một mảng:
nub' xs = runST $ do
arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
let p i = readArray arr i <* writeArray arr i False
filterM p xs
main = print $ nub' [1,2,4,5,4,3,1,8,9,4]
in [1,2,4,5,3,8,9]
như mong đợi.
Và một phiên bản với đơn nguyên IO, yêu cầu trả về các yếu tố nào:
main = filterM p [1,2,4,5] >>= print where
p i = putStrLn ("return " ++ show i ++ "?") *> readLn
Ví dụ
return 1? -- output
True -- input
return 2?
False
return 4?
False
return 5?
True
[1,5] -- output
Và như một minh họa cuối cùng, filterAccum
có thể được định nghĩa theo filterM
:
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
với StateT
đơn nguyên, được sử dụng dưới mui xe, chỉ là một kiểu dữ liệu thông thường.
Ví dụ này minh họa, các đơn nguyên không chỉ cho phép bạn tóm tắt bối cảnh tính toán và viết mã có thể tái sử dụng sạch (do khả năng kết hợp của các đơn vị, như @Carl giải thích), mà còn xử lý các kiểu dữ liệu do người dùng xác định và các nguyên hàm được xây dựng đồng nhất.