Bạn không thể tạo một hàm thuần túy được gọi random
sẽ cho kết quả khác nhau mỗi lần nó được gọi. Trong thực tế, bạn thậm chí không thể "gọi" các hàm thuần túy. Bạn áp dụng chúng. Vì vậy, bạn không thiếu thứ gì, nhưng điều này không có nghĩa là các số ngẫu nhiên nằm ngoài giới hạn trong lập trình chức năng. Cho phép tôi chứng minh, tôi sẽ sử dụng cú pháp Haskell trong suốt.
Đến từ một nền tảng bắt buộc, ban đầu bạn có thể mong đợi ngẫu nhiên có một loại như thế này:
random :: () -> Integer
Nhưng điều này đã được loại trừ vì ngẫu nhiên không thể là một hàm thuần túy.
Hãy xem xét ý tưởng của một giá trị. Một giá trị là một điều bất di bất dịch. Nó không bao giờ thay đổi và mọi quan sát mà bạn có thể thực hiện về nó đều phù hợp với mọi thời đại.
Rõ ràng, ngẫu nhiên không thể tạo ra một giá trị Integer. Thay vào đó, nó tạo ra một biến ngẫu nhiên Integer. Kiểu của nó có thể trông như thế này:
random :: () -> Random Integer
Ngoại trừ việc thông qua một đối số là hoàn toàn không cần thiết, các hàm là thuần túy, vì vậy một hàm random ()
cũng tốt như một đối số khác random ()
. Tôi sẽ đưa ra ngẫu nhiên, từ đây về sau, loại này:
random :: Random Integer
Đó là tất cả tốt và tốt, nhưng không hữu ích. Bạn có thể mong đợi có thể viết các biểu thức như thế nào random + 42
, nhưng bạn không thể, bởi vì nó sẽ không đánh máy. Bạn không thể làm bất cứ điều gì với các biến ngẫu nhiên.
Điều này đặt ra một câu hỏi thú vị. Những chức năng nào nên tồn tại để thao tác các biến ngẫu nhiên?
Chức năng này không thể tồn tại:
bad :: Random a -> a
theo bất kỳ cách hữu ích nào, bởi vì sau đó bạn có thể viết:
badRandom :: Integer
badRandom = bad random
Mà giới thiệu một sự không nhất quán. badRandom được cho là một giá trị, nhưng nó cũng là một con số ngẫu nhiên; một mâu thuẫn.
Có lẽ chúng ta nên thêm chức năng này:
randomAdd :: Integer -> Random Integer -> Random Integer
Nhưng đây chỉ là một trường hợp đặc biệt của một mô hình tổng quát hơn. Bạn sẽ có thể áp dụng bất kỳ chức năng nào cho điều ngẫu nhiên để có được những thứ ngẫu nhiên khác như vậy:
randomMap :: (a -> b) -> Random a -> Random b
Thay vì viết random + 42
, bây giờ chúng ta có thể viết randomMap (+42) random
.
Nếu tất cả những gì bạn có là RandomMap, bạn sẽ không thể kết hợp các biến ngẫu nhiên lại với nhau. Bạn không thể viết hàm này chẳng hạn:
randomCombine :: Random a -> Random b -> Random (a, b)
Bạn có thể thử viết nó như thế này:
randomCombine a b = randomMap (\a' -> randomMap (\b' -> (a', b')) b) a
Nhưng nó có loại sai. Thay vì kết thúc bằng một Random (a, b)
, chúng tôi kết thúc với mộtRandom (Random (a, b))
Điều này có thể được khắc phục bằng cách thêm một chức năng khác:
randomJoin :: Random (Random a) -> Random a
Nhưng, vì những lý do cuối cùng có thể trở nên rõ ràng, tôi sẽ không làm điều đó. Thay vào đó tôi sẽ thêm điều này:
randomBind :: Random a -> (a -> Random b) -> Random b
Không rõ ràng ngay lập tức rằng điều này thực sự giải quyết được vấn đề, nhưng nó thực hiện:
randomCombine a b = randomBind a (\a' -> randomMap (\b' -> (a', b')) b)
Trên thực tế, có thể viết RandomBind theo phương thức RandomJoin và RandomMap. Cũng có thể viết RandomJoin theo phương thức RandomBind. Nhưng, tôi sẽ làm việc này như một bài tập.
Chúng ta có thể đơn giản hóa điều này một chút. Cho phép tôi xác định chức năng này:
randomUnit :: a -> Random a
RandomUnit biến một giá trị thành một biến ngẫu nhiên. Điều này có nghĩa là chúng ta có thể có các biến ngẫu nhiên không thực sự ngẫu nhiên. Đây luôn luôn là trường hợp; chúng ta có thể làm randomMap (const 4) random
trước đây Lý do xác định RandomUnit là một ý tưởng hay là bây giờ chúng ta có thể định nghĩa RandomMap theo thuật ngữ RandomUnit và RandomBind:
randomMap :: (a -> b) -> Random a -> Random b
randomMap f x = randomBind x (randomUnit . f)
Ok, bây giờ chúng tôi đang nhận được ở đâu đó. Chúng ta có các biến ngẫu nhiên mà chúng ta có thể thao tác. Tuy nhiên:
- Không rõ ràng làm thế nào chúng ta thực sự có thể thực hiện các chức năng này,
- Nó khá cồng kềnh.
Thực hiện
Tôi sẽ giải quyết các số giả ngẫu nhiên. Có thể thực hiện các hàm này cho các số ngẫu nhiên thực, nhưng câu trả lời này đã trở nên khá dài.
Về cơ bản, cách thức này sẽ hoạt động là chúng ta sẽ vượt qua một giá trị hạt giống ở khắp mọi nơi. Bất cứ khi nào chúng tôi tạo ra một giá trị ngẫu nhiên mới, chúng tôi sẽ tạo ra một hạt giống mới. Cuối cùng, khi chúng ta hoàn thành việc xây dựng một biến ngẫu nhiên, chúng ta sẽ muốn lấy mẫu từ nó bằng hàm này:
runRandom :: Seed -> Random a -> a
Tôi sẽ định nghĩa loại Ngẫu nhiên như thế này:
data Random a = Random (Seed -> (Seed, a))
Sau đó, chúng tôi chỉ cần cung cấp các triển khai của RandomUnit, RandomBind, runRandom và ngẫu nhiên khá đơn giản:
randomUnit :: a -> Random a
randomUnit x = Random (\seed -> (seed, x))
randomBind :: Random a -> (a -> Random b) -> Random b
randomBind (Random f) g =
Random (\seed ->
let (seed', x) = f seed
Random g' = g x in
g' seed')
runRandom :: Seed -> Random a -> a
runRandom seed (Random f) = (snd . f) seed
Ngẫu nhiên, tôi sẽ giả sử đã có một loại chức năng:
psuedoRandom :: Seed -> (Seed, Integer)
Trong trường hợp ngẫu nhiên là chỉ Random psuedoRandom
.
Làm cho mọi thứ bớt cồng kềnh
Haskell có cú pháp đường để làm cho những thứ như thế này đẹp hơn trên mắt. Nó được gọi là ký hiệu và để sử dụng tất cả chúng ta phải làm nó tạo ra một thể hiện của Monad for Random.
instance Monad Random where
return = randomUnit
(>>=) = randomBind
Làm xong. randomCombine
từ trước có thể được viết:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = do
a' <- a
b' <- b
return (a', b')
Nếu tôi đang làm điều này cho chính mình, tôi thậm chí sẽ tiến xa hơn một bước so với điều này và tạo ra một ví dụ của Ứng dụng. (Đừng lo lắng nếu điều này vô nghĩa).
instance Functor Random where
fmap = liftM
instance Applicative Random where
pure = return
(<*>) = ap
Sau đó, RandomCombine có thể được viết:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = (,) <$> a <*> b
Bây giờ chúng ta có các trường hợp này, chúng ta có thể sử dụng >>=
thay vì RandomBind, tham gia thay vì RandomJoin, fmap thay vì RandomMap, trả về thay vì RandomUnit. Chúng tôi cũng nhận được toàn bộ tải các chức năng miễn phí.
Nó có đáng không? Bạn có thể tranh luận rằng, đến giai đoạn này, nơi làm việc với các số ngẫu nhiên không hoàn toàn khủng khiếp là khá khó khăn và dài dòng. Chúng ta đã nhận được gì để đổi lấy nỗ lực này?
Phần thưởng ngay lập tức nhất là bây giờ chúng ta có thể thấy chính xác phần nào trong chương trình của chúng ta phụ thuộc vào tính ngẫu nhiên và phần nào hoàn toàn mang tính quyết định. Theo kinh nghiệm của tôi, việc buộc một sự tách biệt nghiêm ngặt như thế này đơn giản hóa mọi thứ vô cùng.
Chúng tôi đã giả định rằng chúng tôi chỉ muốn một mẫu từ mỗi biến ngẫu nhiên mà chúng tôi tạo ra, nhưng nếu nó chỉ ra rằng trong tương lai chúng tôi thực sự muốn thấy nhiều hơn về phân phối, thì điều này là tầm thường. Bạn chỉ có thể sử dụng runRandom rất nhiều lần trên cùng một biến ngẫu nhiên với các hạt giống khác nhau. Tất nhiên, điều này là có thể trong các ngôn ngữ bắt buộc, nhưng trong trường hợp này, chúng ta có thể chắc chắn rằng chúng ta sẽ không thực hiện IO không dự đoán được mỗi khi chúng ta lấy mẫu một biến ngẫu nhiên và chúng ta không phải cẩn thận về việc khởi tạo trạng thái.